@tanstack/hotkeys 0.3.0 → 0.3.1
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/README.md +2 -1
- package/dist/match.cjs +10 -3
- package/dist/match.cjs.map +1 -1
- package/dist/match.d.cts +5 -0
- package/dist/match.d.ts +5 -0
- package/dist/match.js +10 -3
- package/dist/match.js.map +1 -1
- package/package.json +1 -1
- package/src/match.ts +21 -15
package/README.md
CHANGED
|
@@ -54,7 +54,8 @@ Type-safe keyboard shortcuts for the web. Template-string bindings, parsed objec
|
|
|
54
54
|
> You may know **TanStack Hotkeys** by our adapter names, too!
|
|
55
55
|
>
|
|
56
56
|
> - [**React Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/react/react-hotkeys)
|
|
57
|
-
> -
|
|
57
|
+
> - [**Preact Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/preact/preact-hotkeys)
|
|
58
|
+
> - [**Solid Hotkeys**](https://tanstack.com/hotkeys/latest/docs/framework/solid/solid-hotkeys)
|
|
58
59
|
> - Angular Hotkeys – needs a contributor!
|
|
59
60
|
> - Svelte Hotkeys – needs a contributor!
|
|
60
61
|
> - Vue Hotkeys – needs a contributor!
|
package/dist/match.cjs
CHANGED
|
@@ -9,6 +9,11 @@ const require_parse = require('./parse.cjs');
|
|
|
9
9
|
* for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters
|
|
10
10
|
* (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively.
|
|
11
11
|
*
|
|
12
|
+
* Also handles "dead key" events where `event.key` is `'Dead'` instead of the expected
|
|
13
|
+
* character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E,
|
|
14
|
+
* Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts.
|
|
15
|
+
* In these cases, `event.code` is used to determine the physical key.
|
|
16
|
+
*
|
|
12
17
|
* @param event - The KeyboardEvent to check
|
|
13
18
|
* @param hotkey - The hotkey string or ParsedHotkey to match against
|
|
14
19
|
* @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
|
|
@@ -32,13 +37,15 @@ function matchesKeyboardEvent(event, hotkey, platform = require_constants.detect
|
|
|
32
37
|
if (event.metaKey !== parsed.meta) return false;
|
|
33
38
|
const eventKey = require_constants.normalizeKeyName(event.key);
|
|
34
39
|
const hotkeyKey = parsed.key;
|
|
35
|
-
if (eventKey.length === 1 && hotkeyKey.length === 1) {
|
|
40
|
+
if (eventKey !== "Dead" && eventKey.length === 1 && hotkeyKey.length === 1) {
|
|
36
41
|
if (eventKey.toUpperCase() === hotkeyKey.toUpperCase()) return true;
|
|
37
|
-
|
|
42
|
+
}
|
|
43
|
+
if (eventKey === "Dead" || eventKey.length === 1 && hotkeyKey.length === 1) {
|
|
44
|
+
if (event.code?.startsWith("Key")) {
|
|
38
45
|
const codeLetter = event.code.slice(3);
|
|
39
46
|
if (codeLetter.length === 1 && /^[A-Za-z]$/.test(codeLetter)) return codeLetter.toUpperCase() === hotkeyKey.toUpperCase();
|
|
40
47
|
}
|
|
41
|
-
if (event.code
|
|
48
|
+
if (event.code?.startsWith("Digit")) {
|
|
42
49
|
const codeDigit = event.code.slice(5);
|
|
43
50
|
if (codeDigit.length === 1 && /^[0-9]$/.test(codeDigit)) return codeDigit === hotkeyKey;
|
|
44
51
|
}
|
package/dist/match.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"match.cjs","names":["detectPlatform","parseHotkey","normalizeKeyName"],"sources":["../src/match.ts"],"sourcesContent":["import { detectPlatform, normalizeKeyName } from './constants'\nimport { parseHotkey } from './parse'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyCallbackContext,\n ParsedHotkey,\n} from './hotkey'\n\n/**\n * Checks if a KeyboardEvent matches a hotkey.\n *\n * Uses the `key` property from KeyboardEvent for matching, with a fallback to `code`\n * for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters\n * (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively.\n *\n * @param event - The KeyboardEvent to check\n * @param hotkey - The hotkey string or ParsedHotkey to match against\n * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)\n * @returns True if the event matches the hotkey\n *\n * @example\n * ```ts\n * document.addEventListener('keydown', (event) => {\n * if (matchesKeyboardEvent(event, 'Mod+S')) {\n * event.preventDefault()\n * handleSave()\n * }\n * })\n * ```\n */\nexport function matchesKeyboardEvent(\n event: KeyboardEvent,\n hotkey: Hotkey | ParsedHotkey,\n platform: 'mac' | 'windows' | 'linux' = detectPlatform(),\n): boolean {\n const parsed =\n typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey\n\n // Check modifiers\n if (event.ctrlKey !== parsed.ctrl) {\n return false\n }\n if (event.shiftKey !== parsed.shift) {\n return false\n }\n if (event.altKey !== parsed.alt) {\n return false\n }\n if (event.metaKey !== parsed.meta) {\n return false\n }\n\n // Check key (case-insensitive for letters)\n const eventKey = normalizeKeyName(event.key)\n const hotkeyKey = parsed.key\n\n // For single
|
|
1
|
+
{"version":3,"file":"match.cjs","names":["detectPlatform","parseHotkey","normalizeKeyName"],"sources":["../src/match.ts"],"sourcesContent":["import { detectPlatform, normalizeKeyName } from './constants'\nimport { parseHotkey } from './parse'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyCallbackContext,\n ParsedHotkey,\n} from './hotkey'\n\n/**\n * Checks if a KeyboardEvent matches a hotkey.\n *\n * Uses the `key` property from KeyboardEvent for matching, with a fallback to `code`\n * for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters\n * (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively.\n *\n * Also handles \"dead key\" events where `event.key` is `'Dead'` instead of the expected\n * character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E,\n * Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts.\n * In these cases, `event.code` is used to determine the physical key.\n *\n * @param event - The KeyboardEvent to check\n * @param hotkey - The hotkey string or ParsedHotkey to match against\n * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)\n * @returns True if the event matches the hotkey\n *\n * @example\n * ```ts\n * document.addEventListener('keydown', (event) => {\n * if (matchesKeyboardEvent(event, 'Mod+S')) {\n * event.preventDefault()\n * handleSave()\n * }\n * })\n * ```\n */\nexport function matchesKeyboardEvent(\n event: KeyboardEvent,\n hotkey: Hotkey | ParsedHotkey,\n platform: 'mac' | 'windows' | 'linux' = detectPlatform(),\n): boolean {\n const parsed =\n typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey\n\n // Check modifiers\n if (event.ctrlKey !== parsed.ctrl) {\n return false\n }\n if (event.shiftKey !== parsed.shift) {\n return false\n }\n if (event.altKey !== parsed.alt) {\n return false\n }\n if (event.metaKey !== parsed.meta) {\n return false\n }\n\n // Check key (case-insensitive for letters)\n const eventKey = normalizeKeyName(event.key)\n const hotkeyKey = parsed.key\n\n // For single-character keys (not dead keys), try direct event.key match first\n if (eventKey !== 'Dead' && eventKey.length === 1 && hotkeyKey.length === 1) {\n if (eventKey.toUpperCase() === hotkeyKey.toUpperCase()) {\n return true\n }\n }\n\n // Fallback to event.code for dead keys or single-char mismatches.\n // Dead keys: Option+letter on macOS, international layouts produce event.key === 'Dead'\n // Single-char mismatches: Cmd+Option+T gives '†' instead of 'T', Shift+4 gives '$'\n if (\n eventKey === 'Dead' ||\n (eventKey.length === 1 && hotkeyKey.length === 1)\n ) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- event.code can be undefined in older browsers/mobile\n if (event.code?.startsWith('Key')) {\n const codeLetter = event.code.slice(3)\n if (codeLetter.length === 1 && /^[A-Za-z]$/.test(codeLetter)) {\n return codeLetter.toUpperCase() === hotkeyKey.toUpperCase()\n }\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- event.code can be undefined in older browsers/mobile\n if (event.code?.startsWith('Digit')) {\n const codeDigit = event.code.slice(5)\n if (codeDigit.length === 1 && /^[0-9]$/.test(codeDigit)) {\n return codeDigit === hotkeyKey\n }\n }\n return false\n }\n\n // For special keys, compare exactly (after normalization)\n return eventKey === hotkeyKey\n}\n\n/**\n * Options for creating a hotkey handler.\n */\nexport interface CreateHotkeyHandlerOptions {\n /** Prevent the default browser action when the hotkey matches. Defaults to true */\n preventDefault?: boolean\n /** Stop event propagation when the hotkey matches. Defaults to true */\n stopPropagation?: boolean\n /** The target platform for resolving 'Mod' */\n platform?: 'mac' | 'windows' | 'linux'\n}\n\n/**\n * Creates a keyboard event handler that calls the callback when the hotkey matches.\n *\n * @param hotkey - The hotkey string or ParsedHotkey to match\n * @param callback - The function to call when the hotkey matches\n * @param options - Options for matching and handling\n * @returns A function that can be used as an event handler\n *\n * @example\n * ```ts\n * const handler = createHotkeyHandler('Mod+S', (event, { hotkey, parsedHotkey }) => {\n * console.log(`${hotkey} was pressed`)\n * handleSave()\n * })\n *\n * document.addEventListener('keydown', handler)\n * ```\n */\nexport function createHotkeyHandler(\n hotkey: Hotkey | ParsedHotkey,\n callback: HotkeyCallback,\n options: CreateHotkeyHandlerOptions = {},\n): (event: KeyboardEvent) => void {\n const { preventDefault = true, stopPropagation = true, platform } = options\n const resolvedPlatform = platform ?? detectPlatform()\n\n const hotkeyString: Hotkey =\n typeof hotkey === 'string' ? hotkey : formatParsedHotkey(hotkey)\n const parsed =\n typeof hotkey === 'string' ? parseHotkey(hotkey, resolvedPlatform) : hotkey\n\n const context: HotkeyCallbackContext = {\n hotkey: hotkeyString,\n parsedHotkey: parsed,\n }\n\n return (event: KeyboardEvent) => {\n if (matchesKeyboardEvent(event, parsed, resolvedPlatform)) {\n if (preventDefault) {\n event.preventDefault()\n }\n if (stopPropagation) {\n event.stopPropagation()\n }\n callback(event, context)\n }\n }\n}\n\ntype MultiHotkeyHandler = { [K in Hotkey]?: HotkeyCallback }\n\n/**\n * Creates a handler that matches multiple hotkeys.\n *\n * @param handlers - A map of hotkey strings to their handlers\n * @param options - Options for matching and handling\n * @returns A function that can be used as an event handler\n *\n * @example\n * ```ts\n * const handler = createMultiHotkeyHandler({\n * 'Mod+S': (event, { hotkey }) => handleSave(),\n * 'Mod+Z': (event, { hotkey }) => handleUndo(),\n * 'Mod+Shift+Z': (event, { hotkey }) => handleRedo(),\n * })\n *\n * document.addEventListener('keydown', handler)\n * ```\n */\nexport function createMultiHotkeyHandler(\n handlers: MultiHotkeyHandler,\n options: CreateHotkeyHandlerOptions = {},\n): (event: KeyboardEvent) => void {\n const { preventDefault = true, stopPropagation = true, platform } = options\n const resolvedPlatform = platform ?? detectPlatform()\n\n // Pre-parse all hotkeys for efficiency\n const parsedHandlers = Object.entries(handlers)\n .filter((entry): entry is [string, HotkeyCallback] => Boolean(entry[1]))\n .map(([hotkey, handler]) => {\n const parsed = parseHotkey(hotkey as Hotkey, resolvedPlatform)\n const context: HotkeyCallbackContext = {\n hotkey: hotkey as Hotkey,\n parsedHotkey: parsed,\n }\n return { parsed, handler, context }\n })\n\n return (event: KeyboardEvent) => {\n for (const { parsed, handler, context } of parsedHandlers) {\n if (matchesKeyboardEvent(event, parsed, resolvedPlatform)) {\n if (preventDefault) {\n event.preventDefault()\n }\n if (stopPropagation) {\n event.stopPropagation()\n }\n handler(event, context)\n return // Only handle the first match\n }\n }\n }\n}\n\n/**\n * Formats a ParsedHotkey back to a hotkey string.\n * Used internally to provide the hotkey string in callback context.\n */\nfunction formatParsedHotkey(parsed: ParsedHotkey): Hotkey {\n const parts: Array<string> = []\n\n if (parsed.ctrl) parts.push('Control')\n if (parsed.alt) parts.push('Alt')\n if (parsed.shift) parts.push('Shift')\n if (parsed.meta) parts.push('Meta')\n parts.push(parsed.key)\n\n return parts.join('+') as Hotkey\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,qBACd,OACA,QACA,WAAwCA,kCAAgB,EAC/C;CACT,MAAM,SACJ,OAAO,WAAW,WAAWC,0BAAY,QAAQ,SAAS,GAAG;AAG/D,KAAI,MAAM,YAAY,OAAO,KAC3B,QAAO;AAET,KAAI,MAAM,aAAa,OAAO,MAC5B,QAAO;AAET,KAAI,MAAM,WAAW,OAAO,IAC1B,QAAO;AAET,KAAI,MAAM,YAAY,OAAO,KAC3B,QAAO;CAIT,MAAM,WAAWC,mCAAiB,MAAM,IAAI;CAC5C,MAAM,YAAY,OAAO;AAGzB,KAAI,aAAa,UAAU,SAAS,WAAW,KAAK,UAAU,WAAW,GACvE;MAAI,SAAS,aAAa,KAAK,UAAU,aAAa,CACpD,QAAO;;AAOX,KACE,aAAa,UACZ,SAAS,WAAW,KAAK,UAAU,WAAW,GAC/C;AAEA,MAAI,MAAM,MAAM,WAAW,MAAM,EAAE;GACjC,MAAM,aAAa,MAAM,KAAK,MAAM,EAAE;AACtC,OAAI,WAAW,WAAW,KAAK,aAAa,KAAK,WAAW,CAC1D,QAAO,WAAW,aAAa,KAAK,UAAU,aAAa;;AAI/D,MAAI,MAAM,MAAM,WAAW,QAAQ,EAAE;GACnC,MAAM,YAAY,MAAM,KAAK,MAAM,EAAE;AACrC,OAAI,UAAU,WAAW,KAAK,UAAU,KAAK,UAAU,CACrD,QAAO,cAAc;;AAGzB,SAAO;;AAIT,QAAO,aAAa;;;;;;;;;;;;;;;;;;;;AAiCtB,SAAgB,oBACd,QACA,UACA,UAAsC,EAAE,EACR;CAChC,MAAM,EAAE,iBAAiB,MAAM,kBAAkB,MAAM,aAAa;CACpE,MAAM,mBAAmB,YAAYF,kCAAgB;CAErD,MAAM,eACJ,OAAO,WAAW,WAAW,SAAS,mBAAmB,OAAO;CAClE,MAAM,SACJ,OAAO,WAAW,WAAWC,0BAAY,QAAQ,iBAAiB,GAAG;CAEvE,MAAM,UAAiC;EACrC,QAAQ;EACR,cAAc;EACf;AAED,SAAQ,UAAyB;AAC/B,MAAI,qBAAqB,OAAO,QAAQ,iBAAiB,EAAE;AACzD,OAAI,eACF,OAAM,gBAAgB;AAExB,OAAI,gBACF,OAAM,iBAAiB;AAEzB,YAAS,OAAO,QAAQ;;;;;;;;;;;;;;;;;;;;;;AAyB9B,SAAgB,yBACd,UACA,UAAsC,EAAE,EACR;CAChC,MAAM,EAAE,iBAAiB,MAAM,kBAAkB,MAAM,aAAa;CACpE,MAAM,mBAAmB,YAAYD,kCAAgB;CAGrD,MAAM,iBAAiB,OAAO,QAAQ,SAAS,CAC5C,QAAQ,UAA6C,QAAQ,MAAM,GAAG,CAAC,CACvE,KAAK,CAAC,QAAQ,aAAa;EAC1B,MAAM,SAASC,0BAAY,QAAkB,iBAAiB;AAK9D,SAAO;GAAE;GAAQ;GAAS,SAJa;IAC7B;IACR,cAAc;IACf;GACkC;GACnC;AAEJ,SAAQ,UAAyB;AAC/B,OAAK,MAAM,EAAE,QAAQ,SAAS,aAAa,eACzC,KAAI,qBAAqB,OAAO,QAAQ,iBAAiB,EAAE;AACzD,OAAI,eACF,OAAM,gBAAgB;AAExB,OAAI,gBACF,OAAM,iBAAiB;AAEzB,WAAQ,OAAO,QAAQ;AACvB;;;;;;;;AAUR,SAAS,mBAAmB,QAA8B;CACxD,MAAM,QAAuB,EAAE;AAE/B,KAAI,OAAO,KAAM,OAAM,KAAK,UAAU;AACtC,KAAI,OAAO,IAAK,OAAM,KAAK,MAAM;AACjC,KAAI,OAAO,MAAO,OAAM,KAAK,QAAQ;AACrC,KAAI,OAAO,KAAM,OAAM,KAAK,OAAO;AACnC,OAAM,KAAK,OAAO,IAAI;AAEtB,QAAO,MAAM,KAAK,IAAI"}
|
package/dist/match.d.cts
CHANGED
|
@@ -8,6 +8,11 @@ import { Hotkey, HotkeyCallback, ParsedHotkey } from "./hotkey.cjs";
|
|
|
8
8
|
* for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters
|
|
9
9
|
* (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively.
|
|
10
10
|
*
|
|
11
|
+
* Also handles "dead key" events where `event.key` is `'Dead'` instead of the expected
|
|
12
|
+
* character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E,
|
|
13
|
+
* Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts.
|
|
14
|
+
* In these cases, `event.code` is used to determine the physical key.
|
|
15
|
+
*
|
|
11
16
|
* @param event - The KeyboardEvent to check
|
|
12
17
|
* @param hotkey - The hotkey string or ParsedHotkey to match against
|
|
13
18
|
* @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
|
package/dist/match.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ import { Hotkey, HotkeyCallback, ParsedHotkey } from "./hotkey.js";
|
|
|
8
8
|
* for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters
|
|
9
9
|
* (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively.
|
|
10
10
|
*
|
|
11
|
+
* Also handles "dead key" events where `event.key` is `'Dead'` instead of the expected
|
|
12
|
+
* character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E,
|
|
13
|
+
* Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts.
|
|
14
|
+
* In these cases, `event.code` is used to determine the physical key.
|
|
15
|
+
*
|
|
11
16
|
* @param event - The KeyboardEvent to check
|
|
12
17
|
* @param hotkey - The hotkey string or ParsedHotkey to match against
|
|
13
18
|
* @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
|
package/dist/match.js
CHANGED
|
@@ -9,6 +9,11 @@ import { parseHotkey } from "./parse.js";
|
|
|
9
9
|
* for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters
|
|
10
10
|
* (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively.
|
|
11
11
|
*
|
|
12
|
+
* Also handles "dead key" events where `event.key` is `'Dead'` instead of the expected
|
|
13
|
+
* character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E,
|
|
14
|
+
* Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts.
|
|
15
|
+
* In these cases, `event.code` is used to determine the physical key.
|
|
16
|
+
*
|
|
12
17
|
* @param event - The KeyboardEvent to check
|
|
13
18
|
* @param hotkey - The hotkey string or ParsedHotkey to match against
|
|
14
19
|
* @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
|
|
@@ -32,13 +37,15 @@ function matchesKeyboardEvent(event, hotkey, platform = detectPlatform()) {
|
|
|
32
37
|
if (event.metaKey !== parsed.meta) return false;
|
|
33
38
|
const eventKey = normalizeKeyName(event.key);
|
|
34
39
|
const hotkeyKey = parsed.key;
|
|
35
|
-
if (eventKey.length === 1 && hotkeyKey.length === 1) {
|
|
40
|
+
if (eventKey !== "Dead" && eventKey.length === 1 && hotkeyKey.length === 1) {
|
|
36
41
|
if (eventKey.toUpperCase() === hotkeyKey.toUpperCase()) return true;
|
|
37
|
-
|
|
42
|
+
}
|
|
43
|
+
if (eventKey === "Dead" || eventKey.length === 1 && hotkeyKey.length === 1) {
|
|
44
|
+
if (event.code?.startsWith("Key")) {
|
|
38
45
|
const codeLetter = event.code.slice(3);
|
|
39
46
|
if (codeLetter.length === 1 && /^[A-Za-z]$/.test(codeLetter)) return codeLetter.toUpperCase() === hotkeyKey.toUpperCase();
|
|
40
47
|
}
|
|
41
|
-
if (event.code
|
|
48
|
+
if (event.code?.startsWith("Digit")) {
|
|
42
49
|
const codeDigit = event.code.slice(5);
|
|
43
50
|
if (codeDigit.length === 1 && /^[0-9]$/.test(codeDigit)) return codeDigit === hotkeyKey;
|
|
44
51
|
}
|
package/dist/match.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"match.js","names":[],"sources":["../src/match.ts"],"sourcesContent":["import { detectPlatform, normalizeKeyName } from './constants'\nimport { parseHotkey } from './parse'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyCallbackContext,\n ParsedHotkey,\n} from './hotkey'\n\n/**\n * Checks if a KeyboardEvent matches a hotkey.\n *\n * Uses the `key` property from KeyboardEvent for matching, with a fallback to `code`\n * for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters\n * (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively.\n *\n * @param event - The KeyboardEvent to check\n * @param hotkey - The hotkey string or ParsedHotkey to match against\n * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)\n * @returns True if the event matches the hotkey\n *\n * @example\n * ```ts\n * document.addEventListener('keydown', (event) => {\n * if (matchesKeyboardEvent(event, 'Mod+S')) {\n * event.preventDefault()\n * handleSave()\n * }\n * })\n * ```\n */\nexport function matchesKeyboardEvent(\n event: KeyboardEvent,\n hotkey: Hotkey | ParsedHotkey,\n platform: 'mac' | 'windows' | 'linux' = detectPlatform(),\n): boolean {\n const parsed =\n typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey\n\n // Check modifiers\n if (event.ctrlKey !== parsed.ctrl) {\n return false\n }\n if (event.shiftKey !== parsed.shift) {\n return false\n }\n if (event.altKey !== parsed.alt) {\n return false\n }\n if (event.metaKey !== parsed.meta) {\n return false\n }\n\n // Check key (case-insensitive for letters)\n const eventKey = normalizeKeyName(event.key)\n const hotkeyKey = parsed.key\n\n // For single
|
|
1
|
+
{"version":3,"file":"match.js","names":[],"sources":["../src/match.ts"],"sourcesContent":["import { detectPlatform, normalizeKeyName } from './constants'\nimport { parseHotkey } from './parse'\nimport type {\n Hotkey,\n HotkeyCallback,\n HotkeyCallbackContext,\n ParsedHotkey,\n} from './hotkey'\n\n/**\n * Checks if a KeyboardEvent matches a hotkey.\n *\n * Uses the `key` property from KeyboardEvent for matching, with a fallback to `code`\n * for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters\n * (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively.\n *\n * Also handles \"dead key\" events where `event.key` is `'Dead'` instead of the expected\n * character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E,\n * Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts.\n * In these cases, `event.code` is used to determine the physical key.\n *\n * @param event - The KeyboardEvent to check\n * @param hotkey - The hotkey string or ParsedHotkey to match against\n * @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)\n * @returns True if the event matches the hotkey\n *\n * @example\n * ```ts\n * document.addEventListener('keydown', (event) => {\n * if (matchesKeyboardEvent(event, 'Mod+S')) {\n * event.preventDefault()\n * handleSave()\n * }\n * })\n * ```\n */\nexport function matchesKeyboardEvent(\n event: KeyboardEvent,\n hotkey: Hotkey | ParsedHotkey,\n platform: 'mac' | 'windows' | 'linux' = detectPlatform(),\n): boolean {\n const parsed =\n typeof hotkey === 'string' ? parseHotkey(hotkey, platform) : hotkey\n\n // Check modifiers\n if (event.ctrlKey !== parsed.ctrl) {\n return false\n }\n if (event.shiftKey !== parsed.shift) {\n return false\n }\n if (event.altKey !== parsed.alt) {\n return false\n }\n if (event.metaKey !== parsed.meta) {\n return false\n }\n\n // Check key (case-insensitive for letters)\n const eventKey = normalizeKeyName(event.key)\n const hotkeyKey = parsed.key\n\n // For single-character keys (not dead keys), try direct event.key match first\n if (eventKey !== 'Dead' && eventKey.length === 1 && hotkeyKey.length === 1) {\n if (eventKey.toUpperCase() === hotkeyKey.toUpperCase()) {\n return true\n }\n }\n\n // Fallback to event.code for dead keys or single-char mismatches.\n // Dead keys: Option+letter on macOS, international layouts produce event.key === 'Dead'\n // Single-char mismatches: Cmd+Option+T gives '†' instead of 'T', Shift+4 gives '$'\n if (\n eventKey === 'Dead' ||\n (eventKey.length === 1 && hotkeyKey.length === 1)\n ) {\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- event.code can be undefined in older browsers/mobile\n if (event.code?.startsWith('Key')) {\n const codeLetter = event.code.slice(3)\n if (codeLetter.length === 1 && /^[A-Za-z]$/.test(codeLetter)) {\n return codeLetter.toUpperCase() === hotkeyKey.toUpperCase()\n }\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- event.code can be undefined in older browsers/mobile\n if (event.code?.startsWith('Digit')) {\n const codeDigit = event.code.slice(5)\n if (codeDigit.length === 1 && /^[0-9]$/.test(codeDigit)) {\n return codeDigit === hotkeyKey\n }\n }\n return false\n }\n\n // For special keys, compare exactly (after normalization)\n return eventKey === hotkeyKey\n}\n\n/**\n * Options for creating a hotkey handler.\n */\nexport interface CreateHotkeyHandlerOptions {\n /** Prevent the default browser action when the hotkey matches. Defaults to true */\n preventDefault?: boolean\n /** Stop event propagation when the hotkey matches. Defaults to true */\n stopPropagation?: boolean\n /** The target platform for resolving 'Mod' */\n platform?: 'mac' | 'windows' | 'linux'\n}\n\n/**\n * Creates a keyboard event handler that calls the callback when the hotkey matches.\n *\n * @param hotkey - The hotkey string or ParsedHotkey to match\n * @param callback - The function to call when the hotkey matches\n * @param options - Options for matching and handling\n * @returns A function that can be used as an event handler\n *\n * @example\n * ```ts\n * const handler = createHotkeyHandler('Mod+S', (event, { hotkey, parsedHotkey }) => {\n * console.log(`${hotkey} was pressed`)\n * handleSave()\n * })\n *\n * document.addEventListener('keydown', handler)\n * ```\n */\nexport function createHotkeyHandler(\n hotkey: Hotkey | ParsedHotkey,\n callback: HotkeyCallback,\n options: CreateHotkeyHandlerOptions = {},\n): (event: KeyboardEvent) => void {\n const { preventDefault = true, stopPropagation = true, platform } = options\n const resolvedPlatform = platform ?? detectPlatform()\n\n const hotkeyString: Hotkey =\n typeof hotkey === 'string' ? hotkey : formatParsedHotkey(hotkey)\n const parsed =\n typeof hotkey === 'string' ? parseHotkey(hotkey, resolvedPlatform) : hotkey\n\n const context: HotkeyCallbackContext = {\n hotkey: hotkeyString,\n parsedHotkey: parsed,\n }\n\n return (event: KeyboardEvent) => {\n if (matchesKeyboardEvent(event, parsed, resolvedPlatform)) {\n if (preventDefault) {\n event.preventDefault()\n }\n if (stopPropagation) {\n event.stopPropagation()\n }\n callback(event, context)\n }\n }\n}\n\ntype MultiHotkeyHandler = { [K in Hotkey]?: HotkeyCallback }\n\n/**\n * Creates a handler that matches multiple hotkeys.\n *\n * @param handlers - A map of hotkey strings to their handlers\n * @param options - Options for matching and handling\n * @returns A function that can be used as an event handler\n *\n * @example\n * ```ts\n * const handler = createMultiHotkeyHandler({\n * 'Mod+S': (event, { hotkey }) => handleSave(),\n * 'Mod+Z': (event, { hotkey }) => handleUndo(),\n * 'Mod+Shift+Z': (event, { hotkey }) => handleRedo(),\n * })\n *\n * document.addEventListener('keydown', handler)\n * ```\n */\nexport function createMultiHotkeyHandler(\n handlers: MultiHotkeyHandler,\n options: CreateHotkeyHandlerOptions = {},\n): (event: KeyboardEvent) => void {\n const { preventDefault = true, stopPropagation = true, platform } = options\n const resolvedPlatform = platform ?? detectPlatform()\n\n // Pre-parse all hotkeys for efficiency\n const parsedHandlers = Object.entries(handlers)\n .filter((entry): entry is [string, HotkeyCallback] => Boolean(entry[1]))\n .map(([hotkey, handler]) => {\n const parsed = parseHotkey(hotkey as Hotkey, resolvedPlatform)\n const context: HotkeyCallbackContext = {\n hotkey: hotkey as Hotkey,\n parsedHotkey: parsed,\n }\n return { parsed, handler, context }\n })\n\n return (event: KeyboardEvent) => {\n for (const { parsed, handler, context } of parsedHandlers) {\n if (matchesKeyboardEvent(event, parsed, resolvedPlatform)) {\n if (preventDefault) {\n event.preventDefault()\n }\n if (stopPropagation) {\n event.stopPropagation()\n }\n handler(event, context)\n return // Only handle the first match\n }\n }\n }\n}\n\n/**\n * Formats a ParsedHotkey back to a hotkey string.\n * Used internally to provide the hotkey string in callback context.\n */\nfunction formatParsedHotkey(parsed: ParsedHotkey): Hotkey {\n const parts: Array<string> = []\n\n if (parsed.ctrl) parts.push('Control')\n if (parsed.alt) parts.push('Alt')\n if (parsed.shift) parts.push('Shift')\n if (parsed.meta) parts.push('Meta')\n parts.push(parsed.key)\n\n return parts.join('+') as Hotkey\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,qBACd,OACA,QACA,WAAwC,gBAAgB,EAC/C;CACT,MAAM,SACJ,OAAO,WAAW,WAAW,YAAY,QAAQ,SAAS,GAAG;AAG/D,KAAI,MAAM,YAAY,OAAO,KAC3B,QAAO;AAET,KAAI,MAAM,aAAa,OAAO,MAC5B,QAAO;AAET,KAAI,MAAM,WAAW,OAAO,IAC1B,QAAO;AAET,KAAI,MAAM,YAAY,OAAO,KAC3B,QAAO;CAIT,MAAM,WAAW,iBAAiB,MAAM,IAAI;CAC5C,MAAM,YAAY,OAAO;AAGzB,KAAI,aAAa,UAAU,SAAS,WAAW,KAAK,UAAU,WAAW,GACvE;MAAI,SAAS,aAAa,KAAK,UAAU,aAAa,CACpD,QAAO;;AAOX,KACE,aAAa,UACZ,SAAS,WAAW,KAAK,UAAU,WAAW,GAC/C;AAEA,MAAI,MAAM,MAAM,WAAW,MAAM,EAAE;GACjC,MAAM,aAAa,MAAM,KAAK,MAAM,EAAE;AACtC,OAAI,WAAW,WAAW,KAAK,aAAa,KAAK,WAAW,CAC1D,QAAO,WAAW,aAAa,KAAK,UAAU,aAAa;;AAI/D,MAAI,MAAM,MAAM,WAAW,QAAQ,EAAE;GACnC,MAAM,YAAY,MAAM,KAAK,MAAM,EAAE;AACrC,OAAI,UAAU,WAAW,KAAK,UAAU,KAAK,UAAU,CACrD,QAAO,cAAc;;AAGzB,SAAO;;AAIT,QAAO,aAAa;;;;;;;;;;;;;;;;;;;;AAiCtB,SAAgB,oBACd,QACA,UACA,UAAsC,EAAE,EACR;CAChC,MAAM,EAAE,iBAAiB,MAAM,kBAAkB,MAAM,aAAa;CACpE,MAAM,mBAAmB,YAAY,gBAAgB;CAErD,MAAM,eACJ,OAAO,WAAW,WAAW,SAAS,mBAAmB,OAAO;CAClE,MAAM,SACJ,OAAO,WAAW,WAAW,YAAY,QAAQ,iBAAiB,GAAG;CAEvE,MAAM,UAAiC;EACrC,QAAQ;EACR,cAAc;EACf;AAED,SAAQ,UAAyB;AAC/B,MAAI,qBAAqB,OAAO,QAAQ,iBAAiB,EAAE;AACzD,OAAI,eACF,OAAM,gBAAgB;AAExB,OAAI,gBACF,OAAM,iBAAiB;AAEzB,YAAS,OAAO,QAAQ;;;;;;;;;;;;;;;;;;;;;;AAyB9B,SAAgB,yBACd,UACA,UAAsC,EAAE,EACR;CAChC,MAAM,EAAE,iBAAiB,MAAM,kBAAkB,MAAM,aAAa;CACpE,MAAM,mBAAmB,YAAY,gBAAgB;CAGrD,MAAM,iBAAiB,OAAO,QAAQ,SAAS,CAC5C,QAAQ,UAA6C,QAAQ,MAAM,GAAG,CAAC,CACvE,KAAK,CAAC,QAAQ,aAAa;EAC1B,MAAM,SAAS,YAAY,QAAkB,iBAAiB;AAK9D,SAAO;GAAE;GAAQ;GAAS,SAJa;IAC7B;IACR,cAAc;IACf;GACkC;GACnC;AAEJ,SAAQ,UAAyB;AAC/B,OAAK,MAAM,EAAE,QAAQ,SAAS,aAAa,eACzC,KAAI,qBAAqB,OAAO,QAAQ,iBAAiB,EAAE;AACzD,OAAI,eACF,OAAM,gBAAgB;AAExB,OAAI,gBACF,OAAM,iBAAiB;AAEzB,WAAQ,OAAO,QAAQ;AACvB;;;;;;;;AAUR,SAAS,mBAAmB,QAA8B;CACxD,MAAM,QAAuB,EAAE;AAE/B,KAAI,OAAO,KAAM,OAAM,KAAK,UAAU;AACtC,KAAI,OAAO,IAAK,OAAM,KAAK,MAAM;AACjC,KAAI,OAAO,MAAO,OAAM,KAAK,QAAQ;AACrC,KAAI,OAAO,KAAM,OAAM,KAAK,OAAO;AACnC,OAAM,KAAK,OAAO,IAAI;AAEtB,QAAO,MAAM,KAAK,IAAI"}
|
package/package.json
CHANGED
package/src/match.ts
CHANGED
|
@@ -14,6 +14,11 @@ import type {
|
|
|
14
14
|
* for letter keys (A-Z) and digit keys (0-9) when `key` produces special characters
|
|
15
15
|
* (e.g., macOS Option+letter or Shift+number). Letter keys are matched case-insensitively.
|
|
16
16
|
*
|
|
17
|
+
* Also handles "dead key" events where `event.key` is `'Dead'` instead of the expected
|
|
18
|
+
* character. This commonly occurs on macOS with Option+letter combinations (e.g., Option+E,
|
|
19
|
+
* Option+I, Option+U, Option+N) and on Windows/Linux with international keyboard layouts.
|
|
20
|
+
* In these cases, `event.code` is used to determine the physical key.
|
|
21
|
+
*
|
|
17
22
|
* @param event - The KeyboardEvent to check
|
|
18
23
|
* @param hotkey - The hotkey string or ParsedHotkey to match against
|
|
19
24
|
* @param platform - The target platform for resolving 'Mod' (defaults to auto-detection)
|
|
@@ -55,33 +60,34 @@ export function matchesKeyboardEvent(
|
|
|
55
60
|
const eventKey = normalizeKeyName(event.key)
|
|
56
61
|
const hotkeyKey = parsed.key
|
|
57
62
|
|
|
58
|
-
// For single
|
|
59
|
-
if (eventKey.length === 1 && hotkeyKey.length === 1) {
|
|
60
|
-
// First try matching with event.key
|
|
63
|
+
// For single-character keys (not dead keys), try direct event.key match first
|
|
64
|
+
if (eventKey !== 'Dead' && eventKey.length === 1 && hotkeyKey.length === 1) {
|
|
61
65
|
if (eventKey.toUpperCase() === hotkeyKey.toUpperCase()) {
|
|
62
66
|
return true
|
|
63
67
|
}
|
|
68
|
+
}
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
// Fallback to event.code for dead keys or single-char mismatches.
|
|
71
|
+
// Dead keys: Option+letter on macOS, international layouts produce event.key === 'Dead'
|
|
72
|
+
// Single-char mismatches: Cmd+Option+T gives '†' instead of 'T', Shift+4 gives '$'
|
|
73
|
+
if (
|
|
74
|
+
eventKey === 'Dead' ||
|
|
75
|
+
(eventKey.length === 1 && hotkeyKey.length === 1)
|
|
76
|
+
) {
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- event.code can be undefined in older browsers/mobile
|
|
78
|
+
if (event.code?.startsWith('Key')) {
|
|
79
|
+
const codeLetter = event.code.slice(3)
|
|
70
80
|
if (codeLetter.length === 1 && /^[A-Za-z]$/.test(codeLetter)) {
|
|
71
81
|
return codeLetter.toUpperCase() === hotkeyKey.toUpperCase()
|
|
72
82
|
}
|
|
73
83
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// event.code format for digit keys is "Digit0", "Digit1", etc.
|
|
78
|
-
if (event.code && event.code.startsWith('Digit')) {
|
|
79
|
-
const codeDigit = event.code.slice(5) // Remove "Digit" prefix
|
|
84
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- event.code can be undefined in older browsers/mobile
|
|
85
|
+
if (event.code?.startsWith('Digit')) {
|
|
86
|
+
const codeDigit = event.code.slice(5)
|
|
80
87
|
if (codeDigit.length === 1 && /^[0-9]$/.test(codeDigit)) {
|
|
81
88
|
return codeDigit === hotkeyKey
|
|
82
89
|
}
|
|
83
90
|
}
|
|
84
|
-
|
|
85
91
|
return false
|
|
86
92
|
}
|
|
87
93
|
|