@vielzeug/keymap 2.2.0 → 26.10.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/parser.js CHANGED
@@ -1,74 +1,2 @@
1
- import { KeymapParseError as e } from "./errors.js";
2
- //#region src/parser.ts
3
- function t() {
4
- if (typeof navigator > "u") return "ctrl";
5
- let e = navigator.userAgentData?.platform ?? navigator.platform ?? "";
6
- return /mac/i.test(e) ? "meta" : "ctrl";
7
- }
8
- var n = {
9
- alt: "alt",
10
- cmd: "meta",
11
- command: "meta",
12
- control: "ctrl",
13
- ctrl: "ctrl",
14
- meta: "meta",
15
- opt: "alt",
16
- option: "alt",
17
- shift: "shift",
18
- win: "meta"
19
- }, r = {
20
- del: "delete",
21
- down: "arrowdown",
22
- esc: "escape",
23
- left: "arrowleft",
24
- right: "arrowright",
25
- space: " ",
26
- spacebar: " ",
27
- up: "arrowup"
28
- };
29
- function i(t, i) {
30
- let a = t.trim().split("+").filter(Boolean).map((e) => e.toLowerCase());
31
- if (a.length === 0) throw new e(`Invalid shortcut step: "${t}"`);
32
- let o = {
33
- ...n,
34
- mod: i
35
- }, s = /* @__PURE__ */ new Set(), c = [];
36
- for (let e of a) {
37
- let t = Object.hasOwn(o, e) ? o[e] : void 0;
38
- t ? s.add(t) : c.push(e);
39
- }
40
- if (c.length === 0) throw new e(`Invalid shortcut step: "${t}"`);
41
- if (c.length > 1) throw new e(`Ambiguous shortcut step: "${t}" — multiple non-modifier keys found`);
42
- let l = c[0];
43
- return {
44
- key: Object.hasOwn(r, l) ? r[l] : l,
45
- modifiers: s
46
- };
47
- }
48
- function a(e, n = t()) {
49
- try {
50
- return i(e, n);
51
- } catch {
52
- return null;
53
- }
54
- }
55
- function o(n, r = t()) {
56
- let a = n.trim().split(/\s+/).filter(Boolean);
57
- if (a.length === 0) throw new e("Shortcut must contain at least one key step");
58
- return a.map((e) => i(e, r));
59
- }
60
- function s(e) {
61
- return e.map((e) => {
62
- let t = [...e.modifiers].sort().join("+");
63
- return t ? `${t}+${e.key}` : e.key;
64
- }).join(" ");
65
- }
66
- function c(e, t) {
67
- if (typeof e.key != "string" || e.key.toLowerCase() !== t.key) return !1;
68
- let { modifiers: n } = t;
69
- return e.altKey === n.has("alt") && e.ctrlKey === n.has("ctrl") && e.metaKey === n.has("meta") && e.shiftKey === n.has("shift");
70
- }
71
- //#endregion
72
- export { s as canonicalizeShortcut, t as detectModKey, c as matchStep, o as parseShortcut, a as parseStep };
73
-
1
+ import{KeymapParseError as e}from"./errors.js";function detectModKey(){if(typeof navigator>`u`)return`ctrl`;let e=navigator.userAgentData?.platform??navigator.platform??``;return/mac/i.test(e)?`meta`:`ctrl`}var t={alt:`alt`,cmd:`meta`,command:`meta`,control:`ctrl`,ctrl:`ctrl`,meta:`meta`,opt:`alt`,option:`alt`,shift:`shift`,win:`meta`},n={del:`delete`,down:`arrowdown`,esc:`escape`,left:`arrowleft`,right:`arrowright`,space:` `,spacebar:` `,up:`arrowup`};function parseStepStrict(r,i){let a=r.trim().split(`+`).filter(Boolean).map(e=>e.toLowerCase());if(a.length===0)throw new e(`Invalid shortcut step: "${r}"`);let o={...t,mod:i},s=new Set,c=[];for(let e of a){let t=Object.hasOwn(o,e)?o[e]:void 0;t?s.add(t):c.push(e)}if(c.length===0)throw new e(`Invalid shortcut step: "${r}"`);if(c.length>1)throw new e(`Ambiguous shortcut step: "${r}" — multiple non-modifier keys found`);let l=c[0];return{key:Object.hasOwn(n,l)?n[l]:l,modifiers:s}}function parseStep(e,t=detectModKey()){try{return parseStepStrict(e,t)}catch{return null}}function parseShortcut(t,n=detectModKey()){let r=t.trim().split(/\s+/).filter(Boolean);if(r.length===0)throw new e(`Shortcut must contain at least one key step`);return r.map(e=>parseStepStrict(e,n))}function canonicalizeShortcut(e){return e.map(e=>{let t=[...e.modifiers].sort().join(`+`);return t?`${t}+${e.key}`:e.key}).join(` `)}function matchStep(e,t){if(typeof e.key!=`string`||e.key.toLowerCase()!==t.key)return!1;let{modifiers:n}=t;return e.altKey===n.has(`alt`)&&e.ctrlKey===n.has(`ctrl`)&&e.metaKey===n.has(`meta`)&&e.shiftKey===n.has(`shift`)}function matchKey(e,t,n=detectModKey()){let r=parseStep(t,n);return r!==null&&matchStep(e,r)}export{canonicalizeShortcut,detectModKey,matchKey,matchStep,parseShortcut,parseStep};
74
2
  //# sourceMappingURL=parser.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"parser.js","names":[],"sources":["../src/parser.ts"],"sourcesContent":["import { KeymapParseError } from './errors';\n\nexport type ModifierKey = 'alt' | 'ctrl' | 'meta' | 'shift';\n\nexport type ShortcutStep = {\n key: string;\n modifiers: Set<ModifierKey>;\n};\n\nexport type Shortcut = ShortcutStep[];\n\nexport function detectModKey(): 'ctrl' | 'meta' {\n if (typeof navigator === 'undefined') return 'ctrl';\n\n const platform =\n (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData?.platform ??\n navigator.platform ??\n '';\n\n return /mac/i.test(platform) ? 'meta' : 'ctrl';\n}\n\nconst MODIFIER_ALIASES_BASE: Record<string, ModifierKey> = {\n alt: 'alt',\n cmd: 'meta',\n command: 'meta',\n control: 'ctrl',\n ctrl: 'ctrl',\n meta: 'meta',\n opt: 'alt',\n option: 'alt',\n shift: 'shift',\n win: 'meta',\n};\n\nconst SPECIAL_KEY_ALIASES: Record<string, string> = {\n del: 'delete',\n down: 'arrowdown',\n esc: 'escape',\n left: 'arrowleft',\n right: 'arrowright',\n space: ' ',\n spacebar: ' ',\n up: 'arrowup',\n};\n\nfunction parseStepStrict(raw: string, modKey: 'ctrl' | 'meta'): ShortcutStep {\n const parts = raw\n .trim()\n .split('+')\n .filter(Boolean)\n .map((part) => part.toLowerCase());\n\n if (parts.length === 0) throw new KeymapParseError(`Invalid shortcut step: \"${raw}\"`);\n\n const aliases: Record<string, ModifierKey> = { ...MODIFIER_ALIASES_BASE, mod: modKey };\n const modifiers = new Set<ModifierKey>();\n const keyParts: string[] = [];\n\n for (const part of parts) {\n const modifier = Object.hasOwn(aliases, part) ? aliases[part]! : undefined;\n\n if (modifier) modifiers.add(modifier);\n else keyParts.push(part);\n }\n\n if (keyParts.length === 0) throw new KeymapParseError(`Invalid shortcut step: \"${raw}\"`);\n\n if (keyParts.length > 1) {\n throw new KeymapParseError(`Ambiguous shortcut step: \"${raw}\" — multiple non-modifier keys found`);\n }\n\n const rawKey = keyParts[0]!;\n const key = Object.hasOwn(SPECIAL_KEY_ALIASES, rawKey) ? SPECIAL_KEY_ALIASES[rawKey]! : rawKey;\n\n return { key, modifiers };\n}\n\nexport function parseStep(raw: string, modKey: 'ctrl' | 'meta' = detectModKey()): ShortcutStep | null {\n try {\n return parseStepStrict(raw, modKey);\n } catch {\n return null;\n }\n}\n\nexport function parseShortcut(raw: string, modKey: 'ctrl' | 'meta' = detectModKey()): Shortcut {\n const steps = raw.trim().split(/\\s+/).filter(Boolean);\n\n if (steps.length === 0) throw new KeymapParseError('Shortcut must contain at least one key step');\n\n return steps.map((step) => parseStepStrict(step, modKey));\n}\n\nexport function canonicalizeShortcut(steps: readonly ShortcutStep[]): string {\n return steps\n .map((step) => {\n const modifiers = [...step.modifiers].sort().join('+');\n\n return modifiers ? `${modifiers}+${step.key}` : step.key;\n })\n .join(' ');\n}\n\nexport function matchStep(event: KeyboardEvent, step: ShortcutStep): boolean {\n // Headless usage (SSR, non-DOM `EventTarget`s, hand-built test events) means `event` isn't\n // guaranteed to be a real `KeyboardEvent` at runtime even though the type says so — treat a\n // missing/non-string `key` as \"doesn't match\" rather than throwing inside the event listener.\n if (typeof event.key !== 'string') return false;\n\n if (event.key.toLowerCase() !== step.key) return false;\n\n const { modifiers } = step;\n\n return (\n event.altKey === modifiers.has('alt') &&\n event.ctrlKey === modifiers.has('ctrl') &&\n event.metaKey === modifiers.has('meta') &&\n event.shiftKey === modifiers.has('shift')\n );\n}\n"],"mappings":";;AAWA,SAAgB,IAAgC;CAC9C,IAAI,OAAO,YAAc,KAAa,OAAO;CAE7C,IAAM,IACH,UAAoE,eAAe,YACpF,UAAU,YACV;CAEF,OAAO,OAAO,KAAK,CAAQ,IAAI,SAAS;AAC1C;AAEA,IAAM,IAAqD;CACzD,KAAK;CACL,KAAK;CACL,SAAS;CACT,SAAS;CACT,MAAM;CACN,MAAM;CACN,KAAK;CACL,QAAQ;CACR,OAAO;CACP,KAAK;AACP,GAEM,IAA8C;CAClD,KAAK;CACL,MAAM;CACN,KAAK;CACL,MAAM;CACN,OAAO;CACP,OAAO;CACP,UAAU;CACV,IAAI;AACN;AAEA,SAAS,EAAgB,GAAa,GAAuC;CAC3E,IAAM,IAAQ,EACX,KAAK,CAAC,CACN,MAAM,GAAG,CAAC,CACV,OAAO,OAAO,CAAC,CACf,KAAK,MAAS,EAAK,YAAY,CAAC;CAEnC,IAAI,EAAM,WAAW,GAAG,MAAM,IAAI,EAAiB,2BAA2B,EAAI,EAAE;CAEpF,IAAM,IAAuC;EAAE,GAAG;EAAuB,KAAK;CAAO,GAC/E,oBAAY,IAAI,IAAiB,GACjC,IAAqB,CAAC;CAE5B,KAAK,IAAM,KAAQ,GAAO;EACxB,IAAM,IAAW,OAAO,OAAO,GAAS,CAAI,IAAI,EAAQ,KAAS,KAAA;EAEjE,AAAI,IAAU,EAAU,IAAI,CAAQ,IAC/B,EAAS,KAAK,CAAI;CACzB;CAEA,IAAI,EAAS,WAAW,GAAG,MAAM,IAAI,EAAiB,2BAA2B,EAAI,EAAE;CAEvF,IAAI,EAAS,SAAS,GACpB,MAAM,IAAI,EAAiB,6BAA6B,EAAI,qCAAqC;CAGnG,IAAM,IAAS,EAAS;CAGxB,OAAO;EAAE,KAFG,OAAO,OAAO,GAAqB,CAAM,IAAI,EAAoB,KAAW;EAE1E;CAAU;AAC1B;AAEA,SAAgB,EAAU,GAAa,IAA0B,EAAa,GAAwB;CACpG,IAAI;EACF,OAAO,EAAgB,GAAK,CAAM;CACpC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,EAAc,GAAa,IAA0B,EAAa,GAAa;CAC7F,IAAM,IAAQ,EAAI,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO;CAEpD,IAAI,EAAM,WAAW,GAAG,MAAM,IAAI,EAAiB,6CAA6C;CAEhG,OAAO,EAAM,KAAK,MAAS,EAAgB,GAAM,CAAM,CAAC;AAC1D;AAEA,SAAgB,EAAqB,GAAwC;CAC3E,OAAO,EACJ,KAAK,MAAS;EACb,IAAM,IAAY,CAAC,GAAG,EAAK,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG;EAErD,OAAO,IAAY,GAAG,EAAU,GAAG,EAAK,QAAQ,EAAK;CACvD,CAAC,CAAC,CACD,KAAK,GAAG;AACb;AAEA,SAAgB,EAAU,GAAsB,GAA6B;CAM3E,IAFI,OAAO,EAAM,OAAQ,YAErB,EAAM,IAAI,YAAY,MAAM,EAAK,KAAK,OAAO;CAEjD,IAAM,EAAE,iBAAc;CAEtB,OACE,EAAM,WAAW,EAAU,IAAI,KAAK,KACpC,EAAM,YAAY,EAAU,IAAI,MAAM,KACtC,EAAM,YAAY,EAAU,IAAI,MAAM,KACtC,EAAM,aAAa,EAAU,IAAI,OAAO;AAE5C"}
1
+ {"version":3,"file":"parser.js","names":[],"sources":["../src/parser.ts"],"sourcesContent":["import { KeymapParseError } from './errors';\n\nexport type ModifierKey = 'alt' | 'ctrl' | 'meta' | 'shift';\n\nexport type ShortcutStep = {\n key: string;\n modifiers: Set<ModifierKey>;\n};\n\nexport type Shortcut = ShortcutStep[];\n\nexport function detectModKey(): 'ctrl' | 'meta' {\n if (typeof navigator === 'undefined') return 'ctrl';\n\n const platform =\n (navigator as Navigator & { userAgentData?: { platform?: string } }).userAgentData?.platform ??\n navigator.platform ??\n '';\n\n return /mac/i.test(platform) ? 'meta' : 'ctrl';\n}\n\nconst MODIFIER_ALIASES_BASE: Record<string, ModifierKey> = {\n alt: 'alt',\n cmd: 'meta',\n command: 'meta',\n control: 'ctrl',\n ctrl: 'ctrl',\n meta: 'meta',\n opt: 'alt',\n option: 'alt',\n shift: 'shift',\n win: 'meta',\n};\n\nconst SPECIAL_KEY_ALIASES: Record<string, string> = {\n del: 'delete',\n down: 'arrowdown',\n esc: 'escape',\n left: 'arrowleft',\n right: 'arrowright',\n space: ' ',\n spacebar: ' ',\n up: 'arrowup',\n};\n\nfunction parseStepStrict(raw: string, modKey: 'ctrl' | 'meta'): ShortcutStep {\n const parts = raw\n .trim()\n .split('+')\n .filter(Boolean)\n .map((part) => part.toLowerCase());\n\n if (parts.length === 0) throw new KeymapParseError(`Invalid shortcut step: \"${raw}\"`);\n\n const aliases: Record<string, ModifierKey> = { ...MODIFIER_ALIASES_BASE, mod: modKey };\n const modifiers = new Set<ModifierKey>();\n const keyParts: string[] = [];\n\n for (const part of parts) {\n const modifier = Object.hasOwn(aliases, part) ? aliases[part]! : undefined;\n\n if (modifier) modifiers.add(modifier);\n else keyParts.push(part);\n }\n\n if (keyParts.length === 0) throw new KeymapParseError(`Invalid shortcut step: \"${raw}\"`);\n\n if (keyParts.length > 1) {\n throw new KeymapParseError(`Ambiguous shortcut step: \"${raw}\" — multiple non-modifier keys found`);\n }\n\n const rawKey = keyParts[0]!;\n const key = Object.hasOwn(SPECIAL_KEY_ALIASES, rawKey) ? SPECIAL_KEY_ALIASES[rawKey]! : rawKey;\n\n return { key, modifiers };\n}\n\nexport function parseStep(raw: string, modKey: 'ctrl' | 'meta' = detectModKey()): ShortcutStep | null {\n try {\n return parseStepStrict(raw, modKey);\n } catch {\n return null;\n }\n}\n\nexport function parseShortcut(raw: string, modKey: 'ctrl' | 'meta' = detectModKey()): Shortcut {\n const steps = raw.trim().split(/\\s+/).filter(Boolean);\n\n if (steps.length === 0) throw new KeymapParseError('Shortcut must contain at least one key step');\n\n return steps.map((step) => parseStepStrict(step, modKey));\n}\n\nexport function canonicalizeShortcut(steps: readonly ShortcutStep[]): string {\n return steps\n .map((step) => {\n const modifiers = [...step.modifiers].sort().join('+');\n\n return modifiers ? `${modifiers}+${step.key}` : step.key;\n })\n .join(' ');\n}\n\nexport function matchStep(event: KeyboardEvent, step: ShortcutStep): boolean {\n // Headless usage (SSR, non-DOM `EventTarget`s, hand-built test events) means `event` isn't\n // guaranteed to be a real `KeyboardEvent` at runtime even though the type says so — treat a\n // missing/non-string `key` as \"doesn't match\" rather than throwing inside the event listener.\n if (typeof event.key !== 'string') return false;\n\n if (event.key.toLowerCase() !== step.key) return false;\n\n const { modifiers } = step;\n\n return (\n event.altKey === modifiers.has('alt') &&\n event.ctrlKey === modifiers.has('ctrl') &&\n event.metaKey === modifiers.has('meta') &&\n event.shiftKey === modifiers.has('shift')\n );\n}\n\n/**\n * Match a keyboard event against a single shortcut pattern such as `'ArrowDown'`,\n * `'esc'`, `'mod+k'`, or `'shift+Home'`. Aliases and `mod` resolve through the same\n * parser `createKeymap` uses, and modifier state must match exactly: `ArrowDown`\n * does not match Ctrl+ArrowDown. Returns `false` for unparseable patterns instead\n * of throwing, so it is safe inside event listeners.\n */\nexport function matchKey(event: KeyboardEvent, pattern: string, modKey: 'ctrl' | 'meta' = detectModKey()): boolean {\n const step = parseStep(pattern, modKey);\n\n return step !== null && matchStep(event, step);\n}\n"],"mappings":"+CAWA,SAAgB,cAAgC,CAC9C,GAAI,OAAO,UAAc,IAAa,MAAO,OAE7C,IAAM,EACH,UAAoE,eAAe,UACpF,UAAU,UACV,GAEF,MAAO,OAAO,KAAK,CAAQ,EAAI,OAAS,MAC1C,CAEA,IAAM,EAAqD,CACzD,IAAK,MACL,IAAK,OACL,QAAS,OACT,QAAS,OACT,KAAM,OACN,KAAM,OACN,IAAK,MACL,OAAQ,MACR,MAAO,QACP,IAAK,MACP,EAEM,EAA8C,CAClD,IAAK,SACL,KAAM,YACN,IAAK,SACL,KAAM,YACN,MAAO,aACP,MAAO,IACP,SAAU,IACV,GAAI,SACN,EAEA,SAAS,gBAAgB,EAAa,EAAuC,CAC3E,IAAM,EAAQ,EACX,KAAK,CAAC,CACN,MAAM,GAAG,CAAC,CACV,OAAO,OAAO,CAAC,CACf,IAAK,GAAS,EAAK,YAAY,CAAC,EAEnC,GAAI,EAAM,SAAW,EAAG,MAAM,IAAI,EAAiB,2BAA2B,EAAI,EAAE,EAEpF,IAAM,EAAuC,CAAE,GAAG,EAAuB,IAAK,CAAO,EAC/E,EAAY,IAAI,IAChB,EAAqB,CAAC,EAE5B,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAM,EAAW,OAAO,OAAO,EAAS,CAAI,EAAI,EAAQ,GAAS,IAAA,GAE7D,EAAU,EAAU,IAAI,CAAQ,EAC/B,EAAS,KAAK,CAAI,CACzB,CAEA,GAAI,EAAS,SAAW,EAAG,MAAM,IAAI,EAAiB,2BAA2B,EAAI,EAAE,EAEvF,GAAI,EAAS,OAAS,EACpB,MAAM,IAAI,EAAiB,6BAA6B,EAAI,qCAAqC,EAGnG,IAAM,EAAS,EAAS,GAGxB,MAAO,CAAE,IAFG,OAAO,OAAO,EAAqB,CAAM,EAAI,EAAoB,GAAW,EAE1E,WAAU,CAC1B,CAEA,SAAgB,UAAU,EAAa,EAA0B,aAAa,EAAwB,CACpG,GAAI,CACF,OAAO,gBAAgB,EAAK,CAAM,CACpC,MAAQ,CACN,OAAO,IACT,CACF,CAEA,SAAgB,cAAc,EAAa,EAA0B,aAAa,EAAa,CAC7F,IAAM,EAAQ,EAAI,KAAK,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,OAAO,OAAO,EAEpD,GAAI,EAAM,SAAW,EAAG,MAAM,IAAI,EAAiB,6CAA6C,EAEhG,OAAO,EAAM,IAAK,GAAS,gBAAgB,EAAM,CAAM,CAAC,CAC1D,CAEA,SAAgB,qBAAqB,EAAwC,CAC3E,OAAO,EACJ,IAAK,GAAS,CACb,IAAM,EAAY,CAAC,GAAG,EAAK,SAAS,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,GAAG,EAErD,OAAO,EAAY,GAAG,EAAU,GAAG,EAAK,MAAQ,EAAK,GACvD,CAAC,CAAC,CACD,KAAK,GAAG,CACb,CAEA,SAAgB,UAAU,EAAsB,EAA6B,CAM3E,GAFI,OAAO,EAAM,KAAQ,UAErB,EAAM,IAAI,YAAY,IAAM,EAAK,IAAK,MAAO,GAEjD,GAAM,CAAE,aAAc,EAEtB,OACE,EAAM,SAAW,EAAU,IAAI,KAAK,GACpC,EAAM,UAAY,EAAU,IAAI,MAAM,GACtC,EAAM,UAAY,EAAU,IAAI,MAAM,GACtC,EAAM,WAAa,EAAU,IAAI,OAAO,CAE5C,CASA,SAAgB,SAAS,EAAsB,EAAiB,EAA0B,aAAa,EAAY,CACjH,IAAM,EAAO,UAAU,EAAS,CAAM,EAEtC,OAAO,IAAS,MAAQ,UAAU,EAAO,CAAI,CAC/C"}
package/dist/types.d.ts CHANGED
@@ -1,9 +1,4 @@
1
1
  import type { ShortcutStep } from './parser';
2
- export type BindingEntry = {
3
- readonly shortcut: readonly ShortcutStep[];
4
- readonly trigger: 'keydown' | 'keyup';
5
- };
6
- export type Handler = (event: KeyboardEvent) => void;
7
2
  /**
8
3
  * Guard function for fine-grained binding context.
9
4
  *
@@ -20,59 +15,73 @@ export type Handler = (event: KeyboardEvent) => void;
20
15
  * { when: (e) => !isModalOpen() }
21
16
  *
22
17
  * // Per-binding: only fire when event target is the specific panel
23
- * { 'escape': { handler: closePanel, when: (e) => e.target === panel } }
18
+ * { id: 'close-panel', shortcut: 'escape', handler: closePanel, when: (e) => e.target === panel }
24
19
  *
25
20
  * Both conditions must be met for the handler to execute.
26
21
  */
27
22
  export type When = (event: KeyboardEvent) => boolean;
28
- export type BindingOptions = {
29
- handler: Handler;
30
- trigger?: 'keydown' | 'keyup';
31
- when?: When;
23
+ export type Handler = (event: KeyboardEvent) => void;
24
+ export type KeymapEvent = {
25
+ readonly target: EventTarget;
26
+ readonly trigger: 'keydown' | 'keyup';
27
+ readonly type: 'chord-cancel';
28
+ } | {
29
+ readonly step: ShortcutStep;
30
+ readonly target: EventTarget;
31
+ readonly trigger: 'keydown' | 'keyup';
32
+ readonly type: 'chord-start';
33
+ } | {
34
+ readonly steps: readonly ShortcutStep[];
35
+ readonly target: EventTarget;
36
+ readonly trigger: 'keydown' | 'keyup';
37
+ readonly type: 'chord-progress';
38
+ } | {
39
+ readonly target: EventTarget;
40
+ readonly trigger: 'keydown' | 'keyup';
41
+ readonly type: 'chord-timeout';
42
+ } | {
43
+ readonly binding: BindingEntry;
44
+ readonly target: EventTarget;
45
+ readonly trigger: 'keydown' | 'keyup';
46
+ readonly type: 'match';
47
+ } | {
48
+ readonly type: 'dispose';
32
49
  };
33
- export type BindingValue = Handler | BindingOptions;
34
50
  /**
35
- * Chord state change event emitted when chord progression changes.
51
+ * A single ordered binding.
36
52
  *
37
- * Use `onChordState` callback in `KeymapOptions` to observe chord state
38
- * for debugging, UI hints, or chord history. When a chord fully matches,
39
- * the binding handler fires immediately; no separate 'completed' event.
53
+ * Each binding has an explicit `id` (string) so duplicate shortcuts can coexist
54
+ * with different ids — `unbind(id)` removes by id, not by shortcut string.
55
+ *
56
+ * @example
57
+ * const map = createKeymap([
58
+ * { id: 'save', shortcut: 'mod+s', handler: save },
59
+ * { id: 'palette', shortcut: 'mod+shift+p', handler: openPalette },
60
+ * { id: 'top', shortcut: 'g g', handler: goToTop },
61
+ * { id: 'close', shortcut: 'escape', handler: closePanel, when: (e) => !isEditableTarget(e.target) },
62
+ * { id: 'play', shortcut: 'space', handler: togglePlay, trigger: 'keyup' },
63
+ * ], { modKey: 'ctrl' });
40
64
  */
41
- export type ChordStateChange = {
42
- type: 'started';
43
- target: EventTarget;
44
- step: ShortcutStep;
45
- trigger: 'keydown' | 'keyup';
46
- } | {
47
- type: 'progressed';
48
- target: EventTarget;
49
- steps: readonly ShortcutStep[];
50
- trigger: 'keydown' | 'keyup';
51
- } | {
52
- type: 'timeout';
53
- target: EventTarget;
54
- trigger: 'keydown' | 'keyup';
55
- };
65
+ export interface Binding {
66
+ /** Handler invoked when the shortcut matches. */
67
+ handler: Handler;
68
+ /** Stable identifier for this binding. Used by `unbind(id)`. */
69
+ id: string;
70
+ /** Call `event.preventDefault()` when matched. Defaults to `true`. */
71
+ preventDefault?: boolean;
72
+ /** Shortcut string, e.g. `'mod+s'` or `'g g'`. */
73
+ shortcut: string;
74
+ /** Call `event.stopPropagation()` when matched. Defaults to `false`. */
75
+ stopPropagation?: boolean;
76
+ /** Event phase. Defaults to `'keydown'`. */
77
+ trigger?: 'keydown' | 'keyup';
78
+ /** Per-binding guard. Both global and per-binding guards must pass. */
79
+ when?: When;
80
+ }
56
81
  export interface KeymapOptions {
82
+ /** Milliseconds to wait for the next step of a chord before the partial chord expires. Defaults to 1000. Must be a positive finite number; `createKeymap` throws `KeymapConfigError` otherwise. */
57
83
  chordTimeout?: number;
58
84
  modKey?: 'ctrl' | 'meta';
59
- /**
60
- * Optional callback to observe chord state changes.
61
- *
62
- * Fires when a chord starts, progresses through steps, completes, or times out.
63
- * Useful for debugging, logging, testing, or implementing chord UI hints.
64
- *
65
- * @example
66
- * {
67
- * onChordState: (change) => {
68
- * if (change.type === 'started') console.log('Chord started:', change.step.key);
69
- * if (change.type === 'timeout') console.log('Chord timed out');
70
- * }
71
- * }
72
- */
73
- onChordState?: (change: ChordStateChange) => void;
74
- preventDefault?: boolean;
75
- stopPropagation?: boolean;
76
85
  /**
77
86
  * Guard function for all bindings in this keymap.
78
87
  *
@@ -85,13 +94,25 @@ export interface KeymapOptions {
85
94
  when?: When;
86
95
  }
87
96
  export interface Keymap {
88
- bind(shortcut: string, value: BindingValue): () => void;
97
+ /** Adds a binding (replacing any existing binding with the same id) and returns an unbind closure. */
98
+ bind(binding: Binding): () => void;
89
99
  readonly disposalSignal: AbortSignal;
90
100
  dispose(): void;
91
101
  readonly disposed: boolean;
92
102
  listBindings(): readonly BindingEntry[];
93
103
  mount(target: EventTarget): () => void;
94
- unbind(shortcut: string): void;
104
+ tap(handler: (event: KeymapEvent) => void, options?: {
105
+ signal?: AbortSignal;
106
+ }): () => void;
107
+ /** Removes the binding with the given id. Warns in development when unknown. */
108
+ unbind(id: string): void;
95
109
  [Symbol.dispose](): void;
96
110
  }
111
+ export type BindingEntry = {
112
+ readonly id: string;
113
+ readonly shortcut: readonly ShortcutStep[];
114
+ readonly trigger: 'keydown' | 'keyup';
115
+ readonly preventDefault: boolean;
116
+ readonly stopPropagation: boolean;
117
+ };
97
118
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,YAAY,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAErD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,IAAI,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,OAAO,CAAC;AAErD,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC;IAC9B,IAAI,CAAC,EAAE,IAAI,CAAC;CACb,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,cAAc,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAA;CAAE,GAC1F;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAA;CAAE,GACzG;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAA;CAAE,CAAC;AAE3E,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAEzB;;;;;;;;;;;;;OAaG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAClD,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AAED,MAAM,WAAW,MAAM;IACrB,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,IAAI,CAAC;IACxD,QAAQ,CAAC,cAAc,EAAE,WAAW,CAAC;IACrC,OAAO,IAAI,IAAI,CAAC;IAChB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,YAAY,IAAI,SAAS,YAAY,EAAE,CAAC;IACxC,KAAK,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,IAAI,CAAC;IACvC,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;CAC1B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vielzeug/keymap",
3
- "version": "2.2.0",
4
- "description": "Headless keyboard shortcut manager with chord sequences, context guards, and disposable bindings",
3
+ "version": "26.10.0",
4
+ "description": "Headless keyboard shortcut manager with chord sequences",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/helmuthdu/vielzeug",
@@ -22,6 +22,18 @@
22
22
  "import": "./dist/index.js",
23
23
  "require": "./dist/index.cjs",
24
24
  "types": "./dist/index.d.ts"
25
+ },
26
+ "./parse": {
27
+ "import": "./dist/parse.js",
28
+ "require": "./dist/parse.cjs",
29
+ "types": "./dist/parse.d.ts"
30
+ }
31
+ },
32
+ "typesVersions": {
33
+ "*": {
34
+ "parse": [
35
+ "dist/parse.d.ts"
36
+ ]
25
37
  }
26
38
  },
27
39
  "sideEffects": false,
@@ -30,18 +42,18 @@
30
42
  "registry": "https://registry.npmjs.org/"
31
43
  },
32
44
  "scripts": {
33
- "build": "vite build && pnpm run build:bundle && pnpm run build:types",
45
+ "build": "vite build && pnpm --silent run build:bundle && pnpm --silent run build:types",
34
46
  "build:bundle": "vite build --config vite.bundle.config.ts",
35
47
  "build:types": "tsc -p tsconfig.declarations.json",
36
48
  "fix": "biome check --write src",
37
49
  "lint": "biome ci src",
38
- "prepublishOnly": "pnpm run build",
50
+ "prepublishOnly": "pnpm --silent run build",
39
51
  "test": "vitest"
40
52
  },
41
53
  "devDependencies": {
42
- "@types/node": "^26.2.0",
54
+ "@types/node": "^26.6.2",
43
55
  "typescript": "^6.0.3",
44
- "vite": "^8.2.1",
45
- "vitest": "^4.1.10"
56
+ "vite": "^8.3.0",
57
+ "vitest": "^5.0.1"
46
58
  }
47
59
  }