hotkey-router 0.0.3 → 0.2.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.
@@ -1,12 +1,22 @@
1
- // hotkey-router v0.0.3 | SEE LICENSE IN LICENSE
1
+ // hotkey-router v0.2.0 | SEE LICENSE IN LICENSE
2
2
 
3
+
4
+ // hotkey-router.js
3
5
  function on(target, type, fn, options) {
4
6
  target.addEventListener(type, fn, options);
5
7
  return () => target.removeEventListener(type, fn, options);
6
8
  }
7
- const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
8
- const modifierTokens = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "meta"]);
9
- const ahkPrefixMap = /* @__PURE__ */ new Map([
9
+ var isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
10
+ var modifierTokens = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "meta"]);
11
+ var MODIFIER_KEY_TOKEN = Object.freeze({
12
+ Alt: "alt",
13
+ AltGraph: "alt",
14
+ Control: "ctrl",
15
+ Shift: "shift",
16
+ Meta: "meta",
17
+ OS: "meta"
18
+ });
19
+ var ahkPrefixMap = /* @__PURE__ */ new Map([
10
20
  ["^", "ctrl"],
11
21
  // Ctrl
12
22
  ["!", "alt"],
@@ -16,7 +26,7 @@ const ahkPrefixMap = /* @__PURE__ */ new Map([
16
26
  ["#", "meta"]
17
27
  // Win / Command
18
28
  ]);
19
- const keyAliases = Object.freeze({
29
+ var keyAliases = Object.freeze({
20
30
  // common
21
31
  esc: "escape",
22
32
  escape: "escape",
@@ -60,27 +70,32 @@ const keyAliases = Object.freeze({
60
70
  // plus key ergonomics
61
71
  plus: "+"
62
72
  });
63
- let ignoreEditable = true;
64
- let defaultTarget = typeof window !== "undefined" ? window : null;
65
- const registry = /* @__PURE__ */ new Map();
66
- const plugins = /* @__PURE__ */ new Map();
67
- const bindingIndex = /* @__PURE__ */ new Map();
68
- let paused = false;
69
- let nextId = 1;
73
+ var ignoreEditable = true;
74
+ var defaultTarget = typeof window !== "undefined" ? window : null;
75
+ var bindHooks = [];
76
+ var registry = /* @__PURE__ */ new Map();
77
+ var plugins = /* @__PURE__ */ new Map();
78
+ var bindingIndex = /* @__PURE__ */ new Map();
79
+ var paused = false;
80
+ var nextId = 1;
70
81
  function normalizeKey(key) {
71
82
  const raw = String(key ?? "").trim();
72
83
  const k = raw.toLowerCase();
73
84
  return keyAliases[k] ?? (raw === " " ? " " : k);
74
85
  }
75
- function comboKeyFromParts({ ctrl, shift, alt, meta, key }) {
86
+ function comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {
87
+ if (bareModifier) return `__bare__:${bareModifier}`;
76
88
  let s = "";
77
89
  if (ctrl) s += "ctrl+";
78
90
  if (shift) s += "shift+";
79
91
  if (alt) s += "alt+";
80
92
  if (meta) s += "meta+";
93
+ if (code) return s + "code:" + code;
81
94
  return s ? s + key : key;
82
95
  }
83
96
  function comboKeyFromEvent(e) {
97
+ const bareMod = MODIFIER_KEY_TOKEN[e.key];
98
+ if (bareMod) return `__bare__:${bareMod}`;
84
99
  return comboKeyFromParts({
85
100
  ctrl: e.ctrlKey,
86
101
  shift: e.shiftKey,
@@ -89,22 +104,52 @@ function comboKeyFromEvent(e) {
89
104
  key: normalizeKey(e.key)
90
105
  });
91
106
  }
107
+ function codeKeyFromEvent(e) {
108
+ if (!e.code) return null;
109
+ let s = "";
110
+ if (e.ctrlKey) s += "ctrl+";
111
+ if (e.shiftKey) s += "shift+";
112
+ if (e.altKey) s += "alt+";
113
+ if (e.metaKey) s += "meta+";
114
+ return s + "code:" + e.code;
115
+ }
92
116
  function isModifierToken(token) {
93
117
  const t = normalizeKey(token);
94
118
  return modifierTokens.has(t);
95
119
  }
120
+ var CODE_TOKEN_RE = /(^|\+)code:([A-Za-z][A-Za-z0-9_]*)/;
96
121
  function parseHotkey(hotkeyStr) {
97
122
  const raw = String(hotkeyStr || "").trim();
98
- const lower = raw.toLowerCase();
99
- const isUp = lower.endsWith(" up");
100
- let cleanStr = (isUp ? raw.slice(0, -3) : raw).trim().toLowerCase();
101
- const combo = { ctrl: false, shift: false, alt: false, meta: false, key: null };
102
- while (cleanStr.length) {
103
- const mod = ahkPrefixMap.get(cleanStr[0]) ?? null;
123
+ const isUp = /\s+up$/i.test(raw);
124
+ const baseRaw = isUp ? raw.replace(/\s+up$/i, "").trim() : raw;
125
+ const combo = {
126
+ ctrl: false,
127
+ shift: false,
128
+ alt: false,
129
+ meta: false,
130
+ key: null,
131
+ code: null,
132
+ bareModifier: null
133
+ };
134
+ let stripped = baseRaw;
135
+ while (stripped.length) {
136
+ const mod = ahkPrefixMap.get(stripped[0]) ?? null;
104
137
  if (!mod) break;
105
138
  combo[mod] = true;
106
- cleanStr = cleanStr.slice(1).trimStart();
139
+ stripped = stripped.slice(1).trimStart();
107
140
  }
141
+ const codeMatches = stripped.match(/code:[A-Za-z][A-Za-z0-9_]*/g);
142
+ if (codeMatches && codeMatches.length > 1) {
143
+ throw new Error(
144
+ `Invalid hotkey "${hotkeyStr}": multiple code: tokens not supported (got ${codeMatches.length}).`
145
+ );
146
+ }
147
+ let codeValue = null;
148
+ const scrubbed = stripped.replace(CODE_TOKEN_RE, (_, sep, val) => {
149
+ codeValue = val;
150
+ return sep + "__code__";
151
+ });
152
+ let cleanStr = scrubbed.toLowerCase();
108
153
  const rawParts = cleanStr.split("+").map((p) => p.trim());
109
154
  const parts = rawParts.filter(Boolean);
110
155
  const impliedPlusKey = rawParts.some((p) => p === "") && cleanStr.includes("++");
@@ -123,6 +168,10 @@ function parseHotkey(hotkeyStr) {
123
168
  }
124
169
  }
125
170
  for (const part of parts) {
171
+ if (part === "__code__") {
172
+ combo.code = codeValue;
173
+ continue;
174
+ }
126
175
  const actual = keyAliases[part] ?? part;
127
176
  if (actual === "ctrl") combo.ctrl = true;
128
177
  else if (actual === "shift") combo.shift = true;
@@ -130,8 +179,19 @@ function parseHotkey(hotkeyStr) {
130
179
  else if (actual === "meta") combo.meta = true;
131
180
  else combo.key = normalizeKey(actual);
132
181
  }
133
- if (!combo.key && impliedPlusKey) combo.key = "+";
134
- if (!combo.key) throw new Error(`Invalid hotkey "${hotkeyStr}" (missing base key)`);
182
+ if (!combo.key && !combo.code && impliedPlusKey) combo.key = "+";
183
+ if (!combo.key && !combo.code) {
184
+ const modCount = (combo.ctrl ? 1 : 0) + (combo.shift ? 1 : 0) + (combo.alt ? 1 : 0) + (combo.meta ? 1 : 0);
185
+ if (modCount === 1) {
186
+ combo.bareModifier = combo.ctrl ? "ctrl" : combo.shift ? "shift" : combo.alt ? "alt" : "meta";
187
+ } else if (modCount > 1) {
188
+ throw new Error(
189
+ `Invalid hotkey "${hotkeyStr}": multi-modifier bare bindings not supported. Add a base key (e.g. "ctrl+alt+x").`
190
+ );
191
+ } else {
192
+ throw new Error(`Invalid hotkey "${hotkeyStr}" (missing base key)`);
193
+ }
194
+ }
135
195
  return { combo, type: isUp ? "keyup" : "keydown", raw: hotkeyStr };
136
196
  }
137
197
  function isFromInputTarget(e) {
@@ -192,11 +252,29 @@ function bind(hotkeyStr, handler, plugin = null, options = {}) {
192
252
  };
193
253
  ensureSlot(comboStr, type).push(binding);
194
254
  bindingIndex.set(binding.id, { comboStr, type });
255
+ if (bindHooks.length) {
256
+ for (const hook of bindHooks) {
257
+ try {
258
+ hook({ combo, raw, options: opts, plugin, id: binding.id });
259
+ } catch (err) {
260
+ console.error("[hotkey-router] onBind hook threw:", err);
261
+ }
262
+ }
263
+ }
195
264
  const off = () => removeById(binding.id);
196
265
  off.id = binding.id;
197
266
  off.hotkey = raw;
198
267
  return off;
199
268
  }
269
+ function onBind(hook) {
270
+ if (typeof hook !== "function") {
271
+ throw new TypeError("hotkeys.onBind(hook) expects a function");
272
+ }
273
+ bindHooks.push(hook);
274
+ return () => {
275
+ bindHooks = bindHooks.filter((h) => h !== hook);
276
+ };
277
+ }
200
278
  function unbind(hotkeyStr, handler) {
201
279
  const { combo, type } = parseHotkey(hotkeyStr);
202
280
  const comboStr = comboKeyFromParts(combo);
@@ -261,15 +339,21 @@ function handleEvent(type) {
261
339
  if (paused) return;
262
340
  const fromInput = ignoreEditable && isFromInputTarget(e);
263
341
  const comboStr = comboKeyFromEvent(e);
264
- const bindings = registry.get(comboStr)?.get(type);
265
- if (!bindings?.length) return;
342
+ const codeStr = codeKeyFromEvent(e);
343
+ const seen = /* @__PURE__ */ new Set();
266
344
  const candidates = [];
267
- for (const b of bindings) {
268
- const o2 = b.options;
269
- if (fromInput && !o2.allowIn?.(e)) continue;
270
- if (type === "keydown" && o2.repeat === false && e.repeat) continue;
271
- if (o2.when && o2.when(e) === false) continue;
272
- candidates.push(b);
345
+ for (const lookupKey of [comboStr, codeStr]) {
346
+ if (!lookupKey || seen.has(lookupKey)) continue;
347
+ seen.add(lookupKey);
348
+ const bindings = registry.get(lookupKey)?.get(type);
349
+ if (!bindings?.length) continue;
350
+ for (const b of bindings) {
351
+ const o2 = b.options;
352
+ if (fromInput && !o2.allowIn?.(e)) continue;
353
+ if (type === "keydown" && o2.repeat === false && e.repeat) continue;
354
+ if (o2.when && o2.when(e) === false) continue;
355
+ candidates.push(b);
356
+ }
273
357
  }
274
358
  if (!candidates.length) return;
275
359
  const winner = pickWinner(candidates);
@@ -281,8 +365,8 @@ function handleEvent(type) {
281
365
  if (o.once) removeById(winner.id);
282
366
  };
283
367
  }
284
- let stopDown = null;
285
- let stopUp = null;
368
+ var stopDown = null;
369
+ var stopUp = null;
286
370
  function init({ target = defaultTarget, capture = false } = {}) {
287
371
  if (!target) throw new Error("hotkeys.init() requires a target (e.g. window)");
288
372
  if (stopDown || stopUp) destroy();
@@ -298,19 +382,37 @@ function destroy() {
298
382
  plugins.clear();
299
383
  bindingIndex.clear();
300
384
  }
385
+ var BARE_MOD_TO_KEY = {
386
+ ctrl: "Control",
387
+ shift: "Shift",
388
+ alt: "Alt",
389
+ meta: "Meta"
390
+ };
301
391
  function trigger(hotkeyStr, { type: forcedType } = {}) {
302
392
  const { combo, type } = parseHotkey(hotkeyStr);
303
393
  const comboStr = comboKeyFromParts(combo);
304
394
  const actualType = forcedType || type;
305
395
  const bindings = registry.get(comboStr)?.get(actualType);
306
396
  if (!bindings?.length) return false;
397
+ const eventKey = combo.bareModifier ? BARE_MOD_TO_KEY[combo.bareModifier] : combo.key;
398
+ let altKey = !!combo.alt;
399
+ let ctrlKey = !!combo.ctrl;
400
+ let shiftKey = !!combo.shift;
401
+ let metaKey = !!combo.meta;
402
+ if (combo.bareModifier && actualType === "keyup") {
403
+ if (combo.bareModifier === "alt") altKey = false;
404
+ else if (combo.bareModifier === "ctrl") ctrlKey = false;
405
+ else if (combo.bareModifier === "shift") shiftKey = false;
406
+ else if (combo.bareModifier === "meta") metaKey = false;
407
+ }
307
408
  const e = {
308
409
  type: actualType,
309
- key: combo.key,
310
- ctrlKey: !!combo.ctrl,
311
- shiftKey: !!combo.shift,
312
- altKey: !!combo.alt,
313
- metaKey: !!combo.meta,
410
+ key: eventKey,
411
+ code: combo.code || null,
412
+ ctrlKey,
413
+ shiftKey,
414
+ altKey,
415
+ metaKey,
314
416
  repeat: false,
315
417
  preventDefault() {
316
418
  },
@@ -339,6 +441,8 @@ var hotkey_router_default = {
339
441
  unbind,
340
442
  registerPlugin,
341
443
  unregisterPlugin,
444
+ // extension hooks
445
+ onBind,
342
446
  // control
343
447
  pause,
344
448
  resume,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../hotkey-router.js"],
4
- "sourcesContent": ["// hotkey-router.js\r\n// Hotkey Router \u2014 plugin-first keyboard shortcut router (tiny + predictable)\r\n//\r\n// Goals:\r\n// - O(1) dispatch by combo string lookup\r\n// - Deterministic routing: highest priority wins; ties -> most recently bound wins\r\n// - Safe plugin cleanup (never deletes other plugins\u2019 bindings)\r\n// - Modern browser target (KeyboardEvent.key + addEventListener)\r\n\r\n// --- tiny event helper ---\r\nfunction on(target, type, fn, options) {\r\n target.addEventListener(type, fn, options)\r\n return () => target.removeEventListener(type, fn, options)\r\n}\r\n\r\n// --- key alias map ---\r\nconst isMac =\r\n typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform)\r\n\r\n\r\nconst modifierTokens = new Set(['ctrl', 'shift', 'alt', 'meta'])\r\n\r\nconst ahkPrefixMap = new Map([\r\n ['^', 'ctrl'], // Ctrl\r\n ['!', 'alt'], // Alt\r\n ['+', 'shift'], // Shift\r\n ['#', 'meta'], // Win / Command\r\n])\r\n\r\n\r\nconst keyAliases = Object.freeze({\r\n // common\r\n esc: 'escape',\r\n escape: 'escape',\r\n space: ' ',\r\n spacebar: ' ',\r\n enter: 'enter',\r\n return: 'enter',\r\n del: 'delete',\r\n delete: 'delete',\r\n backspace: 'backspace',\r\n tab: 'tab',\r\n\r\n // arrows\r\n up: 'arrowup',\r\n down: 'arrowdown',\r\n left: 'arrowleft',\r\n right: 'arrowright',\r\n\r\n // navigation\r\n home: 'home',\r\n end: 'end',\r\n pageup: 'pageup',\r\n pagedown: 'pagedown',\r\n pgup: 'pageup',\r\n pgdn: 'pagedown',\r\n\r\n // modifiers (words)\r\n cmd: 'meta',\r\n command: 'meta',\r\n win: 'meta',\r\n meta: 'meta',\r\n control: 'ctrl',\r\n ctrl: 'ctrl',\r\n option: 'alt',\r\n alt: 'alt',\r\n shift: 'shift',\r\n\r\n // modifiers (symbols)\r\n '\u2318': 'meta',\r\n '\u2303': 'ctrl',\r\n '\u2325': 'alt',\r\n '\u21E7': 'shift',\r\n\r\n // cross-platform \"mod\"\r\n mod: isMac ? 'meta' : 'ctrl',\r\n\r\n // plus key ergonomics\r\n plus: '+',\r\n})\r\n\r\n// --- config ---\r\nlet ignoreEditable = true\r\nlet defaultTarget = typeof window !== 'undefined' ? window : null\r\n\r\n// --- state ---\r\n// comboStr -> Map<type, Array<Binding>>\r\nconst registry = new Map()\r\n// pluginName -> Array<bindingId>\r\nconst plugins = new Map()\r\n// bindingId -> { comboStr, type }\r\nconst bindingIndex = new Map()\r\n\r\nlet paused = false\r\nlet nextId = 1\r\n\r\n// --- helpers ---\r\nfunction normalizeKey(key) {\r\n const raw = String(key ?? '').trim()\r\n const k = raw.toLowerCase()\r\n return keyAliases[k] ?? (raw === ' ' ? ' ' : k)\r\n}\r\n\r\nfunction comboKeyFromParts({ ctrl, shift, alt, meta, key }) {\r\n let s = ''\r\n if (ctrl) s += 'ctrl+'\r\n if (shift) s += 'shift+'\r\n if (alt) s += 'alt+'\r\n if (meta) s += 'meta+'\r\n return s ? s + key : key\r\n}\r\n\r\nfunction comboKeyFromEvent(e) {\r\n return comboKeyFromParts({\r\n ctrl: e.ctrlKey,\r\n shift: e.shiftKey,\r\n alt: e.altKey,\r\n meta: e.metaKey,\r\n key: normalizeKey(e.key),\r\n })\r\n}\r\n\r\n\r\nfunction isModifierToken(token) {\r\n const t = normalizeKey(token)\r\n return modifierTokens.has(t)\r\n}\r\n\r\n\r\nfunction parseHotkey(hotkeyStr) {\r\n const raw = String(hotkeyStr || '').trim()\r\n const lower = raw.toLowerCase()\r\n\r\n const isUp = lower.endsWith(' up')\r\n let cleanStr = (isUp ? raw.slice(0, -3) : raw).trim().toLowerCase()\r\n\r\n // --- AHK-style prefix modifiers ---\r\n // Examples:\r\n // ^k => ctrl+k\r\n // !k => alt+k\r\n // +k => shift+k\r\n // #k => meta+k\r\n // ^!k => ctrl+alt+k\r\n //\r\n // This runs BEFORE normal \"+\" token parsing so it composes cleanly.\r\n const combo = { ctrl: false, shift: false, alt: false, meta: false, key: null }\r\n\r\n while (cleanStr.length) {\r\n const mod = ahkPrefixMap.get(cleanStr[0]) ?? null\r\n if (!mod) break\r\n combo[mod] = true\r\n cleanStr = cleanStr.slice(1).trimStart()\r\n }\r\n\r\n // Allow plus key:\r\n // - \"ctrl++\" => ctrl + \"+\"\r\n // - \"++\" => shift + \"+\" (AHK '+' prefix sets shift, remaining '+' becomes the base key)\r\n // `split('+')` produces empties; we keep that signal.\r\n const rawParts = cleanStr.split('+').map((p) => p.trim())\r\n const parts = rawParts.filter(Boolean)\r\n const impliedPlusKey = rawParts.some((p) => p === '') && cleanStr.includes('++')\r\n\r\n // Enforce: modifiers must come before the base key.\r\n // Example: \"ctrl+k\" \u2705 | \"k+ctrl\" \u274C\r\n // We ignore the special \"+ key\" case (ctrl++) since it has no explicit base token.\r\n if (!impliedPlusKey) {\r\n let seenBase = false\r\n for (const part of parts) {\r\n if (isModifierToken(part)) {\r\n if (seenBase) {\r\n throw new Error(\r\n `Invalid hotkey \"${hotkeyStr}\": modifiers must come before the base key (e.g. \"ctrl+k\", not \"k+ctrl\")`\r\n )\r\n }\r\n } else {\r\n if (!seenBase) seenBase = true\r\n }\r\n }\r\n }\r\n\r\n // Continue parsing the remaining tokens (words/symbols/aliases)\r\n for (const part of parts) {\r\n const actual = keyAliases[part] ?? part\r\n if (actual === 'ctrl') combo.ctrl = true\r\n else if (actual === 'shift') combo.shift = true\r\n else if (actual === 'alt') combo.alt = true\r\n else if (actual === 'meta') combo.meta = true\r\n else combo.key = normalizeKey(actual)\r\n }\r\n\r\n if (!combo.key && impliedPlusKey) combo.key = '+'\r\n if (!combo.key) throw new Error(`Invalid hotkey \"${hotkeyStr}\" (missing base key)`)\r\n\r\n return { combo, type: isUp ? 'keyup' : 'keydown', raw: hotkeyStr }\r\n}\r\n\r\n\r\nfunction isFromInputTarget(e) {\r\n const t = e?.target\r\n if (!t) return false\r\n const tag = String(t.tagName || '').toUpperCase()\r\n return (\r\n t.isContentEditable ||\r\n tag === 'INPUT' ||\r\n tag === 'TEXTAREA' ||\r\n tag === 'SELECT' ||\r\n t.getAttribute?.('role') === 'textbox'\r\n )\r\n}\r\n\r\n\r\n// --- binding model ---\r\n// options:\r\n// preventDefault, stopPropagation, stopImmediatePropagation\r\n// repeat (default false on keydown), once\r\n// when(e) -> boolean (gate)\r\n// allowIn(e) -> boolean (override ignoreInput)\r\n// priority (higher wins)\r\nfunction normalizeOptions(type, options) {\r\n const o = options ? { ...options } : {}\r\n if (type === 'keydown' && o.repeat == null) o.repeat = false\r\n if (o.priority == null) o.priority = 0\r\n return o\r\n}\r\n\r\n\r\nfunction ensureSlot(comboStr, type) {\r\n let typeMap = registry.get(comboStr)\r\n if (!typeMap) {\r\n typeMap = new Map()\r\n registry.set(comboStr, typeMap)\r\n }\r\n let arr = typeMap.get(type)\r\n if (!arr) {\r\n arr = []\r\n typeMap.set(type, arr)\r\n }\r\n return arr\r\n}\r\n\r\n\r\nfunction removeById(id) {\r\n const idx = bindingIndex.get(id)\r\n if (!idx) return false\r\n\r\n const { comboStr, type } = idx\r\n const typeMap = registry.get(comboStr)\r\n const arr = typeMap?.get(type)\r\n if (!arr?.length) {\r\n bindingIndex.delete(id)\r\n return false\r\n }\r\n\r\n const next = arr.filter((b) => b.id !== id)\r\n if (next.length) typeMap.set(type, next)\r\n else typeMap.delete(type)\r\n\r\n if (!typeMap.size) registry.delete(comboStr)\r\n bindingIndex.delete(id)\r\n return true\r\n}\r\n\r\n\r\n// --- public API: bind / unbind ---\r\n// bind returns an off() function; off.id is exposed for plugin bookkeeping.\r\nfunction bind(hotkeyStr, handler, plugin = null, options = {}) {\r\n const { combo, type, raw } = parseHotkey(hotkeyStr)\r\n const comboStr = comboKeyFromParts(combo)\r\n const opts = normalizeOptions(type, options)\r\n\r\n const binding = {\r\n id: nextId++,\r\n comboStr,\r\n type,\r\n combo,\r\n handler,\r\n plugin,\r\n raw,\r\n options: opts,\r\n }\r\n\r\n ensureSlot(comboStr, type).push(binding)\r\n bindingIndex.set(binding.id, { comboStr, type })\r\n\r\n const off = () => removeById(binding.id)\r\n off.id = binding.id\r\n off.hotkey = raw\r\n return off\r\n}\r\n\r\n\r\n// unbind('ctrl+k') -> remove all for that combo/type\r\n// unbind('ctrl+k', fn) -> remove only that handler\r\nfunction unbind(hotkeyStr, handler) {\r\n const { combo, type } = parseHotkey(hotkeyStr)\r\n const comboStr = comboKeyFromParts(combo)\r\n const typeMap = registry.get(comboStr)\r\n if (!typeMap) return\r\n\r\n const arr = typeMap.get(type)\r\n if (!arr?.length) return\r\n\r\n if (!handler) {\r\n for (const b of arr) bindingIndex.delete(b.id)\r\n typeMap.delete(type)\r\n } else {\r\n const keep = []\r\n for (const b of arr) {\r\n if (b.handler === handler) bindingIndex.delete(b.id)\r\n else keep.push(b)\r\n }\r\n if (keep.length) typeMap.set(type, keep)\r\n else typeMap.delete(type)\r\n }\r\n\r\n if (!typeMap.size) registry.delete(comboStr)\r\n}\r\n\r\n\r\n// --- plugin system ---\r\n// registerPlugin returns unregister function.\r\nfunction registerPlugin(name, hotkeyMap) {\r\n if (plugins.has(name)) throw new Error(`Plugin \"${name}\" already registered`)\r\n const ids = []\r\n\r\n for (const [hotkeyStr, fnOrTuple] of Object.entries(hotkeyMap)) {\r\n const [fn, options] = Array.isArray(fnOrTuple) ? fnOrTuple : [fnOrTuple, {}]\r\n const off = bind(hotkeyStr, fn, name, options)\r\n ids.push(off.id)\r\n }\r\n\r\n plugins.set(name, ids)\r\n return () => unregisterPlugin(name)\r\n}\r\n\r\nfunction unregisterPlugin(name) {\r\n const ids = plugins.get(name)\r\n if (!ids) return\r\n for (const id of ids) removeById(id)\r\n plugins.delete(name)\r\n}\r\n\r\n// --- control ---\r\nfunction pause() {\r\n paused = true\r\n}\r\n\r\nfunction resume() {\r\n paused = false\r\n}\r\n\r\nfunction ignoreInput(value = true) {\r\n ignoreEditable = !!value\r\n}\r\n\r\n// allow changing the default target (iframes/tests).\r\n// we intentionally do not auto-rebind to keep the core tiny.\r\nfunction setTarget(target) {\r\n defaultTarget = target\r\n}\r\n\r\n\r\n// --- dispatch (O(1) lookup) ---\r\nfunction pickWinner(bindings) {\r\n // Highest priority wins; ties -> newest wins (largest id).\r\n let best = null\r\n for (const b of bindings) {\r\n if (!best) best = b\r\n else if (b.options.priority > best.options.priority) best = b\r\n else if (b.options.priority === best.options.priority && b.id > best.id) best = b\r\n }\r\n return best\r\n}\r\n\r\n\r\nfunction handleEvent(type) {\r\n return (e) => {\r\n if (paused) return\r\n\r\n const fromInput = ignoreEditable && isFromInputTarget(e)\r\n const comboStr = comboKeyFromEvent(e)\r\n\r\n const bindings = registry.get(comboStr)?.get(type)\r\n if (!bindings?.length) return\r\n\r\n const candidates = []\r\n for (const b of bindings) {\r\n const o = b.options\r\n if (fromInput && !o.allowIn?.(e)) continue\r\n if (type === 'keydown' && o.repeat === false && e.repeat) continue\r\n if (o.when && o.when(e) === false) continue\r\n candidates.push(b)\r\n }\r\n if (!candidates.length) return\r\n\r\n const winner = pickWinner(candidates)\r\n const o = winner.options\r\n\r\n if (o.preventDefault) e.preventDefault()\r\n if (o.stopImmediatePropagation) e.stopImmediatePropagation()\r\n else if (o.stopPropagation) e.stopPropagation()\r\n\r\n winner.handler(e)\r\n\r\n if (o.once) removeById(winner.id)\r\n }\r\n}\r\n\r\n\r\n// --- listeners ---\r\nlet stopDown = null\r\nlet stopUp = null\r\n\r\nfunction init({ target = defaultTarget, capture = false } = {}) {\r\n if (!target) throw new Error('hotkeys.init() requires a target (e.g. window)')\r\n if (stopDown || stopUp) destroy()\r\n\r\n stopDown = on(target, 'keydown', handleEvent('keydown'), { capture })\r\n stopUp = on(target, 'keyup', handleEvent('keyup'), { capture })\r\n}\r\n\r\n\r\nfunction destroy() {\r\n stopDown?.()\r\n stopUp?.()\r\n stopDown = null\r\n stopUp = null\r\n registry.clear()\r\n plugins.clear()\r\n bindingIndex.clear()\r\n}\r\n\r\n\r\n// --- programmatic trigger (tests / automation) ---\r\nfunction trigger(hotkeyStr, { type: forcedType } = {}) {\r\n const { combo, type } = parseHotkey(hotkeyStr)\r\n const comboStr = comboKeyFromParts(combo)\r\n const actualType = forcedType || type\r\n\r\n const bindings = registry.get(comboStr)?.get(actualType)\r\n if (!bindings?.length) return false\r\n\r\n const e = {\r\n type: actualType,\r\n key: combo.key,\r\n ctrlKey: !!combo.ctrl,\r\n shiftKey: !!combo.shift,\r\n altKey: !!combo.alt,\r\n metaKey: !!combo.meta,\r\n repeat: false,\r\n preventDefault() { },\r\n stopPropagation() { },\r\n stopImmediatePropagation() { },\r\n target: null,\r\n }\r\n\r\n const winner = pickWinner(\r\n bindings.filter((b) => (b.options.when ? b.options.when(e) !== false : true))\r\n )\r\n if (!winner) return false\r\n\r\n winner.handler(e)\r\n if (winner.options.once) removeById(winner.id)\r\n return true\r\n}\r\n\r\n\r\n// Auto-init on window by default (browser only)\r\nif (defaultTarget) init()\r\n\r\nexport default {\r\n // lifecycle\r\n init,\r\n destroy,\r\n setTarget,\r\n\r\n // bindings\r\n bind,\r\n unbind,\r\n registerPlugin,\r\n unregisterPlugin,\r\n\r\n // control\r\n pause,\r\n resume,\r\n ignoreInput,\r\n\r\n // testing / automation\r\n trigger,\r\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["// hotkey-router.js\n// Hotkey Router \u2014 plugin-first keyboard shortcut router (tiny + predictable)\n//\n// Goals:\n// - O(1) dispatch by combo string lookup\n// - Deterministic routing: highest priority wins; ties -> most recently bound wins\n// - Safe plugin cleanup (never deletes other plugins\u2019 bindings)\n// - Modern browser target (KeyboardEvent.key + addEventListener)\n//\n// This core ships ZERO reservation data \u2014 opt in via `hotkey-router/auto`\n// or by calling `installReservationWarnings(hotkeys)` from the separate\n// `hotkey-router/reservations` module. Bundlers tree-shake the data file\n// out entirely when neither is imported.\n\n// --- tiny event helper ---\nfunction on(target, type, fn, options) {\n target.addEventListener(type, fn, options)\n return () => target.removeEventListener(type, fn, options)\n}\n\n// --- key alias map ---\nconst isMac =\n typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform)\n\n\nconst modifierTokens = new Set(['ctrl', 'shift', 'alt', 'meta'])\n\n// Map from KeyboardEvent.key (which IS the modifier name on bare-modifier events)\n// to the canonical token used in combo strings. Used to detect bare-modifier\n// keydown/keyup events at dispatch time.\nconst MODIFIER_KEY_TOKEN = Object.freeze({\n Alt: 'alt',\n AltGraph: 'alt',\n Control: 'ctrl',\n Shift: 'shift',\n Meta: 'meta',\n OS: 'meta',\n})\n\nconst ahkPrefixMap = new Map([\n ['^', 'ctrl'], // Ctrl\n ['!', 'alt'], // Alt\n ['+', 'shift'], // Shift\n ['#', 'meta'], // Win / Command\n])\n\n\nconst keyAliases = Object.freeze({\n // common\n esc: 'escape',\n escape: 'escape',\n space: ' ',\n spacebar: ' ',\n enter: 'enter',\n return: 'enter',\n del: 'delete',\n delete: 'delete',\n backspace: 'backspace',\n tab: 'tab',\n\n // arrows\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n\n // navigation\n home: 'home',\n end: 'end',\n pageup: 'pageup',\n pagedown: 'pagedown',\n pgup: 'pageup',\n pgdn: 'pagedown',\n\n // modifiers (words)\n cmd: 'meta',\n command: 'meta',\n win: 'meta',\n meta: 'meta',\n control: 'ctrl',\n ctrl: 'ctrl',\n option: 'alt',\n alt: 'alt',\n shift: 'shift',\n\n // modifiers (symbols)\n '\u2318': 'meta',\n '\u2303': 'ctrl',\n '\u2325': 'alt',\n '\u21E7': 'shift',\n\n // cross-platform \"mod\"\n mod: isMac ? 'meta' : 'ctrl',\n\n // plus key ergonomics\n plus: '+',\n})\n\n// --- config ---\nlet ignoreEditable = true\nlet defaultTarget = typeof window !== 'undefined' ? window : null\n\n// Bind hooks: subscribers called after each successful bind. Used by the\n// optional reservations module to emit conflict warnings without coupling\n// the core router to the (~5 KB) reservation table.\nlet bindHooks = []\n\n// --- state ---\n// comboStr -> Map<type, Array<Binding>>\nconst registry = new Map()\n// pluginName -> Array<bindingId>\nconst plugins = new Map()\n// bindingId -> { comboStr, type }\nconst bindingIndex = new Map()\n\nlet paused = false\nlet nextId = 1\n\n// --- helpers ---\nfunction normalizeKey(key) {\n const raw = String(key ?? '').trim()\n const k = raw.toLowerCase()\n return keyAliases[k] ?? (raw === ' ' ? ' ' : k)\n}\n\nfunction comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {\n // Bare-modifier bindings get a sentinel namespace so they can't collide with\n // regular chord combo strings (`'alt+x'` vs `'__bare__:alt'`).\n if (bareModifier) return `__bare__:${bareModifier}`\n\n let s = ''\n if (ctrl) s += 'ctrl+'\n if (shift) s += 'shift+'\n if (alt) s += 'alt+'\n if (meta) s += 'meta+'\n\n // Code-based combos use a `code:KeyX` suffix instead of the key character.\n if (code) return s + 'code:' + code\n return s ? s + key : key\n}\n\nfunction comboKeyFromEvent(e) {\n // If the event's `key` is itself a modifier name, this is a bare-modifier\n // press/release. Match it via the bare-modifier sentinel regardless of which\n // other modifier flags are set (loose match \u2014 Alt fires `alt` even when held\n // alongside Ctrl).\n const bareMod = MODIFIER_KEY_TOKEN[e.key]\n if (bareMod) return `__bare__:${bareMod}`\n\n return comboKeyFromParts({\n ctrl: e.ctrlKey,\n shift: e.shiftKey,\n alt: e.altKey,\n meta: e.metaKey,\n key: normalizeKey(e.key),\n })\n}\n\n// Code-based lookup string for an event. Independent of `comboKeyFromEvent`\n// because consumers can register either key-based OR code-based bindings (or\n// both) for the same physical key combo, and we look up under whichever they\n// chose.\nfunction codeKeyFromEvent(e) {\n if (!e.code) return null\n\n let s = ''\n if (e.ctrlKey) s += 'ctrl+'\n if (e.shiftKey) s += 'shift+'\n if (e.altKey) s += 'alt+'\n if (e.metaKey) s += 'meta+'\n return s + 'code:' + e.code\n}\n\n\nfunction isModifierToken(token) {\n const t = normalizeKey(token)\n return modifierTokens.has(t)\n}\n\n\n// Matches `code:KeyX` (case-sensitive value). Used to extract code tokens BEFORE\n// the rest of the parser lowercases everything, since KeyboardEvent.code values\n// are camelCase (KeyA, Digit1, ArrowLeft) and must be preserved.\nconst CODE_TOKEN_RE = /(^|\\+)code:([A-Za-z][A-Za-z0-9_]*)/\n\nfunction parseHotkey(hotkeyStr) {\n const raw = String(hotkeyStr || '').trim()\n\n // Detect ' up' suffix case-insensitively so 'Alt UP' works the same as 'alt up'.\n const isUp = /\\s+up$/i.test(raw)\n const baseRaw = isUp ? raw.replace(/\\s+up$/i, '').trim() : raw\n\n // --- AHK-style prefix modifiers ---\n // Examples:\n // ^k => ctrl+k\n // !k => alt+k\n // +k => shift+k\n // #k => meta+k\n // ^!k => ctrl+alt+k\n // !code:KeyX => alt+code:KeyX\n //\n // We strip these BEFORE both the code-token extraction AND the lowercase\n // pass, because the AHK prefix chars (^ ! + #) are case-stable, and the\n // remainder may contain a case-sensitive `code:KeyX` token that must survive.\n const combo = {\n ctrl: false, shift: false, alt: false, meta: false,\n key: null, code: null, bareModifier: null,\n }\n\n let stripped = baseRaw\n while (stripped.length) {\n const mod = ahkPrefixMap.get(stripped[0]) ?? null\n if (!mod) break\n combo[mod] = true\n stripped = stripped.slice(1).trimStart()\n }\n\n // Pull out a code:XXX token before lowercasing; preserve the camelCase value.\n // Reject multiple code: tokens to avoid silent loss of intent.\n const codeMatches = stripped.match(/code:[A-Za-z][A-Za-z0-9_]*/g)\n if (codeMatches && codeMatches.length > 1) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": multiple code: tokens not supported (got ${codeMatches.length}).`\n )\n }\n let codeValue = null\n const scrubbed = stripped.replace(CODE_TOKEN_RE, (_, sep, val) => {\n codeValue = val\n return sep + '__code__'\n })\n\n let cleanStr = scrubbed.toLowerCase()\n\n // Allow plus key:\n // - \"ctrl++\" => ctrl + \"+\"\n // - \"++\" => shift + \"+\" (AHK '+' prefix sets shift, remaining '+' becomes the base key)\n // `split('+')` produces empties; we keep that signal.\n const rawParts = cleanStr.split('+').map((p) => p.trim())\n const parts = rawParts.filter(Boolean)\n const impliedPlusKey = rawParts.some((p) => p === '') && cleanStr.includes('++')\n\n // Enforce: modifiers must come before the base key.\n // Example: \"ctrl+k\" \u2705 | \"k+ctrl\" \u274C\n // We ignore the special \"+ key\" case (ctrl++) since it has no explicit base token.\n if (!impliedPlusKey) {\n let seenBase = false\n for (const part of parts) {\n if (isModifierToken(part)) {\n if (seenBase) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": modifiers must come before the base key (e.g. \"ctrl+k\", not \"k+ctrl\")`\n )\n }\n } else {\n if (!seenBase) seenBase = true\n }\n }\n }\n\n // Continue parsing the remaining tokens (words/symbols/aliases)\n for (const part of parts) {\n if (part === '__code__') {\n combo.code = codeValue\n continue\n }\n const actual = keyAliases[part] ?? part\n if (actual === 'ctrl') combo.ctrl = true\n else if (actual === 'shift') combo.shift = true\n else if (actual === 'alt') combo.alt = true\n else if (actual === 'meta') combo.meta = true\n else combo.key = normalizeKey(actual)\n }\n\n if (!combo.key && !combo.code && impliedPlusKey) combo.key = '+'\n\n // If no base key/code is set, this might be a bare-modifier binding ('alt' / 'ctrl').\n // Allow exactly ONE modifier with no other tokens. Multi-modifier bare bindings\n // (e.g. 'ctrl+alt') are intentionally rejected \u2014 add a base key for those.\n if (!combo.key && !combo.code) {\n const modCount =\n (combo.ctrl ? 1 : 0) + (combo.shift ? 1 : 0) +\n (combo.alt ? 1 : 0) + (combo.meta ? 1 : 0)\n\n if (modCount === 1) {\n combo.bareModifier =\n combo.ctrl ? 'ctrl' :\n combo.shift ? 'shift' :\n combo.alt ? 'alt' : 'meta'\n } else if (modCount > 1) {\n throw new Error(\n `Invalid hotkey \"${hotkeyStr}\": multi-modifier bare bindings not supported. Add a base key (e.g. \"ctrl+alt+x\").`\n )\n } else {\n throw new Error(`Invalid hotkey \"${hotkeyStr}\" (missing base key)`)\n }\n }\n\n return { combo, type: isUp ? 'keyup' : 'keydown', raw: hotkeyStr }\n}\n\n\nfunction isFromInputTarget(e) {\n const t = e?.target\n if (!t) return false\n const tag = String(t.tagName || '').toUpperCase()\n return (\n t.isContentEditable ||\n tag === 'INPUT' ||\n tag === 'TEXTAREA' ||\n tag === 'SELECT' ||\n t.getAttribute?.('role') === 'textbox'\n )\n}\n\n\n// --- binding model ---\n// options:\n// preventDefault, stopPropagation, stopImmediatePropagation\n// repeat (default false on keydown), once\n// when(e) -> boolean (gate)\n// allowIn(e) -> boolean (override ignoreInput)\n// priority (higher wins)\nfunction normalizeOptions(type, options) {\n const o = options ? { ...options } : {}\n if (type === 'keydown' && o.repeat == null) o.repeat = false\n if (o.priority == null) o.priority = 0\n return o\n}\n\n\nfunction ensureSlot(comboStr, type) {\n let typeMap = registry.get(comboStr)\n if (!typeMap) {\n typeMap = new Map()\n registry.set(comboStr, typeMap)\n }\n let arr = typeMap.get(type)\n if (!arr) {\n arr = []\n typeMap.set(type, arr)\n }\n return arr\n}\n\n\nfunction removeById(id) {\n const idx = bindingIndex.get(id)\n if (!idx) return false\n\n const { comboStr, type } = idx\n const typeMap = registry.get(comboStr)\n const arr = typeMap?.get(type)\n if (!arr?.length) {\n bindingIndex.delete(id)\n return false\n }\n\n const next = arr.filter((b) => b.id !== id)\n if (next.length) typeMap.set(type, next)\n else typeMap.delete(type)\n\n if (!typeMap.size) registry.delete(comboStr)\n bindingIndex.delete(id)\n return true\n}\n\n\n// --- public API: bind / unbind ---\n// bind returns an off() function; off.id is exposed for plugin bookkeeping.\nfunction bind(hotkeyStr, handler, plugin = null, options = {}) {\n const { combo, type, raw } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const opts = normalizeOptions(type, options)\n\n const binding = {\n id: nextId++,\n comboStr,\n type,\n combo,\n handler,\n plugin,\n raw,\n options: opts,\n }\n\n ensureSlot(comboStr, type).push(binding)\n bindingIndex.set(binding.id, { comboStr, type })\n\n // Notify bind subscribers. Hook errors must never break the bind itself.\n if (bindHooks.length) {\n for (const hook of bindHooks) {\n try {\n hook({ combo, raw, options: opts, plugin, id: binding.id })\n } catch (err) {\n // eslint-disable-next-line no-console\n console.error('[hotkey-router] onBind hook threw:', err)\n }\n }\n }\n\n const off = () => removeById(binding.id)\n off.id = binding.id\n off.hotkey = raw\n return off\n}\n\n// Subscribe to bind events. The hook receives\n// { combo, raw, options, plugin, id }\n// after the binding is registered. Returns an unsubscribe function. Used by\n// extensions like `installReservationWarnings` from `hotkey-router/reservations`.\nfunction onBind(hook) {\n if (typeof hook !== 'function') {\n throw new TypeError('hotkeys.onBind(hook) expects a function')\n }\n bindHooks.push(hook)\n return () => {\n bindHooks = bindHooks.filter((h) => h !== hook)\n }\n}\n\n\n// unbind('ctrl+k') -> remove all for that combo/type\n// unbind('ctrl+k', fn) -> remove only that handler\nfunction unbind(hotkeyStr, handler) {\n const { combo, type } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const typeMap = registry.get(comboStr)\n if (!typeMap) return\n\n const arr = typeMap.get(type)\n if (!arr?.length) return\n\n if (!handler) {\n for (const b of arr) bindingIndex.delete(b.id)\n typeMap.delete(type)\n } else {\n const keep = []\n for (const b of arr) {\n if (b.handler === handler) bindingIndex.delete(b.id)\n else keep.push(b)\n }\n if (keep.length) typeMap.set(type, keep)\n else typeMap.delete(type)\n }\n\n if (!typeMap.size) registry.delete(comboStr)\n}\n\n\n// --- plugin system ---\n// registerPlugin returns unregister function.\nfunction registerPlugin(name, hotkeyMap) {\n if (plugins.has(name)) throw new Error(`Plugin \"${name}\" already registered`)\n const ids = []\n\n for (const [hotkeyStr, fnOrTuple] of Object.entries(hotkeyMap)) {\n const [fn, options] = Array.isArray(fnOrTuple) ? fnOrTuple : [fnOrTuple, {}]\n const off = bind(hotkeyStr, fn, name, options)\n ids.push(off.id)\n }\n\n plugins.set(name, ids)\n return () => unregisterPlugin(name)\n}\n\nfunction unregisterPlugin(name) {\n const ids = plugins.get(name)\n if (!ids) return\n for (const id of ids) removeById(id)\n plugins.delete(name)\n}\n\n// --- control ---\nfunction pause() {\n paused = true\n}\n\nfunction resume() {\n paused = false\n}\n\nfunction ignoreInput(value = true) {\n ignoreEditable = !!value\n}\n\n// allow changing the default target (iframes/tests).\n// we intentionally do not auto-rebind to keep the core tiny.\nfunction setTarget(target) {\n defaultTarget = target\n}\n\n\n// --- dispatch (O(1) lookup) ---\nfunction pickWinner(bindings) {\n // Highest priority wins; ties -> newest wins (largest id).\n let best = null\n for (const b of bindings) {\n if (!best) best = b\n else if (b.options.priority > best.options.priority) best = b\n else if (b.options.priority === best.options.priority && b.id > best.id) best = b\n }\n return best\n}\n\n\nfunction handleEvent(type) {\n return (e) => {\n if (paused) return\n\n const fromInput = ignoreEditable && isFromInputTarget(e)\n const comboStr = comboKeyFromEvent(e)\n const codeStr = codeKeyFromEvent(e)\n\n // Look up under both lookup keys: key-based (`alt+x`) and code-based\n // (`alt+code:KeyX`). Same physical keypress, two valid binding shapes.\n // Merge candidates and let the existing priority/recency arbiter pick.\n const seen = new Set()\n const candidates = []\n for (const lookupKey of [comboStr, codeStr]) {\n if (!lookupKey || seen.has(lookupKey)) continue\n seen.add(lookupKey)\n\n const bindings = registry.get(lookupKey)?.get(type)\n if (!bindings?.length) continue\n\n for (const b of bindings) {\n const o = b.options\n if (fromInput && !o.allowIn?.(e)) continue\n if (type === 'keydown' && o.repeat === false && e.repeat) continue\n if (o.when && o.when(e) === false) continue\n candidates.push(b)\n }\n }\n if (!candidates.length) return\n\n const winner = pickWinner(candidates)\n const o = winner.options\n\n if (o.preventDefault) e.preventDefault()\n if (o.stopImmediatePropagation) e.stopImmediatePropagation()\n else if (o.stopPropagation) e.stopPropagation()\n\n winner.handler(e)\n\n if (o.once) removeById(winner.id)\n }\n}\n\n\n// --- listeners ---\nlet stopDown = null\nlet stopUp = null\n\nfunction init({ target = defaultTarget, capture = false } = {}) {\n if (!target) throw new Error('hotkeys.init() requires a target (e.g. window)')\n if (stopDown || stopUp) destroy()\n\n stopDown = on(target, 'keydown', handleEvent('keydown'), { capture })\n stopUp = on(target, 'keyup', handleEvent('keyup'), { capture })\n}\n\n\nfunction destroy() {\n stopDown?.()\n stopUp?.()\n stopDown = null\n stopUp = null\n registry.clear()\n plugins.clear()\n bindingIndex.clear()\n // Note: bindHooks deliberately survive destroy(). Consumer extensions\n // (reservations, future telemetry, etc.) install once at startup and\n // shouldn't have to re-attach when the router restarts. Tests that need\n // a clean slate should use the uninstall fn returned by their installer.\n}\n\n\n// --- programmatic trigger (tests / automation) ---\n//\n// Reverse map: 'alt' -> 'Alt' (the KeyboardEvent.key value for that bare modifier).\n// Used to populate the synthetic event's `.key` field so consumer handlers that\n// inspect `e.key` see the same value they'd get from a real event.\nconst BARE_MOD_TO_KEY = {\n ctrl: 'Control',\n shift: 'Shift',\n alt: 'Alt',\n meta: 'Meta',\n}\n\nfunction trigger(hotkeyStr, { type: forcedType } = {}) {\n const { combo, type } = parseHotkey(hotkeyStr)\n const comboStr = comboKeyFromParts(combo)\n const actualType = forcedType || type\n\n const bindings = registry.get(comboStr)?.get(actualType)\n if (!bindings?.length) return false\n\n // Fill `.key` from bareModifier when the binding is bare-modifier-only;\n // otherwise use combo.key. `.code` is set from combo.code for code-based bindings.\n const eventKey = combo.bareModifier\n ? BARE_MOD_TO_KEY[combo.bareModifier]\n : combo.key\n\n // For bare-modifier keyup, real KeyboardEvents have the corresponding\n // modifier flag FALSE (the modifier just released). Mirror that so\n // automation handlers that inspect e.altKey / e.ctrlKey / etc. see the\n // same value they'd see from a real event.\n let altKey = !!combo.alt\n let ctrlKey = !!combo.ctrl\n let shiftKey = !!combo.shift\n let metaKey = !!combo.meta\n if (combo.bareModifier && actualType === 'keyup') {\n if (combo.bareModifier === 'alt') altKey = false\n else if (combo.bareModifier === 'ctrl') ctrlKey = false\n else if (combo.bareModifier === 'shift') shiftKey = false\n else if (combo.bareModifier === 'meta') metaKey = false\n }\n\n const e = {\n type: actualType,\n key: eventKey,\n code: combo.code || null,\n ctrlKey, shiftKey, altKey, metaKey,\n repeat: false,\n preventDefault() { },\n stopPropagation() { },\n stopImmediatePropagation() { },\n target: null,\n }\n\n const winner = pickWinner(\n bindings.filter((b) => (b.options.when ? b.options.when(e) !== false : true))\n )\n if (!winner) return false\n\n winner.handler(e)\n if (winner.options.once) removeById(winner.id)\n return true\n}\n\n\n// Auto-init on window by default (browser only)\nif (defaultTarget) init()\n\nexport default {\n // lifecycle\n init,\n destroy,\n setTarget,\n\n // bindings\n bind,\n unbind,\n registerPlugin,\n unregisterPlugin,\n\n // extension hooks\n onBind,\n\n // control\n pause,\n resume,\n ignoreInput,\n\n // testing / automation\n trigger,\n}"],
5
+ "mappings": "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",
6
6
  "names": ["o"]
7
7
  }
@@ -1,3 +1,3 @@
1
- // hotkey-router v0.0.3 | SEE LICENSE IN LICENSE
1
+ // hotkey-router v0.2.0 | SEE LICENSE IN LICENSE
2
2
 
3
- function E(t,e,n,r){return t.addEventListener(e,n,r),()=>t.removeEventListener(e,n,r)}const C=typeof navigator<"u"&&/Mac|iPhone|iPad|iPod/.test(navigator.platform),L=new Set(["ctrl","shift","alt","meta"]),z=new Map([["^","ctrl"],["!","alt"],["+","shift"],["#","meta"]]),v=Object.freeze({esc:"escape",escape:"escape",space:" ",spacebar:" ",enter:"enter",return:"enter",del:"delete",delete:"delete",backspace:"backspace",tab:"tab",up:"arrowup",down:"arrowdown",left:"arrowleft",right:"arrowright",home:"home",end:"end",pageup:"pageup",pagedown:"pagedown",pgup:"pageup",pgdn:"pagedown",cmd:"meta",command:"meta",win:"meta",meta:"meta",control:"ctrl",ctrl:"ctrl",option:"alt",alt:"alt",shift:"shift","\u2318":"meta","\u2303":"ctrl","\u2325":"alt","\u21E7":"shift",mod:C?"meta":"ctrl",plus:"+"});let I=!0,h=typeof window<"u"?window:null;const u=new Map,d=new Map,p=new Map;let b=!1,D=1;function k(t){const e=String(t??"").trim(),n=e.toLowerCase();return v[n]??(e===" "?" ":n)}function g({ctrl:t,shift:e,alt:n,meta:r,key:i}){let o="";return t&&(o+="ctrl+"),e&&(o+="shift+"),n&&(o+="alt+"),r&&(o+="meta+"),o?o+i:i}function O(t){return g({ctrl:t.ctrlKey,shift:t.shiftKey,alt:t.altKey,meta:t.metaKey,key:k(t.key)})}function U(t){const e=k(t);return L.has(e)}function P(t){const e=String(t||"").trim(),r=e.toLowerCase().endsWith(" up");let i=(r?e.slice(0,-3):e).trim().toLowerCase();const o={ctrl:!1,shift:!1,alt:!1,meta:!1,key:null};for(;i.length;){const s=z.get(i[0])??null;if(!s)break;o[s]=!0,i=i.slice(1).trimStart()}const l=i.split("+").map(s=>s.trim()),a=l.filter(Boolean),f=l.some(s=>s==="")&&i.includes("++");if(!f){let s=!1;for(const c of a)if(U(c)){if(s)throw new Error(`Invalid hotkey "${t}": modifiers must come before the base key (e.g. "ctrl+k", not "k+ctrl")`)}else s||(s=!0)}for(const s of a){const c=v[s]??s;c==="ctrl"?o.ctrl=!0:c==="shift"?o.shift=!0:c==="alt"?o.alt=!0:c==="meta"?o.meta=!0:o.key=k(c)}if(!o.key&&f&&(o.key="+"),!o.key)throw new Error(`Invalid hotkey "${t}" (missing base key)`);return{combo:o,type:r?"keyup":"keydown",raw:t}}function B(t){const e=t?.target;if(!e)return!1;const n=String(e.tagName||"").toUpperCase();return e.isContentEditable||n==="INPUT"||n==="TEXTAREA"||n==="SELECT"||e.getAttribute?.("role")==="textbox"}function F(t,e){const n=e?{...e}:{};return t==="keydown"&&n.repeat==null&&(n.repeat=!1),n.priority==null&&(n.priority=0),n}function $(t,e){let n=u.get(t);n||(n=new Map,u.set(t,n));let r=n.get(e);return r||(r=[],n.set(e,r)),r}function m(t){const e=p.get(t);if(!e)return!1;const{comboStr:n,type:r}=e,i=u.get(n),o=i?.get(r);if(!o?.length)return p.delete(t),!1;const l=o.filter(a=>a.id!==t);return l.length?i.set(r,l):i.delete(r),i.size||u.delete(n),p.delete(t),!0}function K(t,e,n=null,r={}){const{combo:i,type:o,raw:l}=P(t),a=g(i),f=F(o,r),s={id:D++,comboStr:a,type:o,combo:i,handler:e,plugin:n,raw:l,options:f};$(a,o).push(s),p.set(s.id,{comboStr:a,type:o});const c=()=>m(s.id);return c.id=s.id,c.hotkey=l,c}function j(t,e){const{combo:n,type:r}=P(t),i=g(n),o=u.get(i);if(!o)return;const l=o.get(r);if(l?.length){if(e){const a=[];for(const f of l)f.handler===e?p.delete(f.id):a.push(f);a.length?o.set(r,a):o.delete(r)}else{for(const a of l)p.delete(a.id);o.delete(r)}o.size||u.delete(i)}}function N(t,e){if(d.has(t))throw new Error(`Plugin "${t}" already registered`);const n=[];for(const[r,i]of Object.entries(e)){const[o,l]=Array.isArray(i)?i:[i,{}],a=K(r,o,t,l);n.push(a.id)}return d.set(t,n),()=>M(t)}function M(t){const e=d.get(t);if(e){for(const n of e)m(n);d.delete(t)}}function W(){b=!0}function q(){b=!1}function H(t=!0){I=!!t}function R(t){h=t}function T(t){let e=null;for(const n of t)e?(n.options.priority>e.options.priority||n.options.priority===e.options.priority&&n.id>e.id)&&(e=n):e=n;return e}function S(t){return e=>{if(b)return;const n=I&&B(e),r=O(e),i=u.get(r)?.get(t);if(!i?.length)return;const o=[];for(const f of i){const s=f.options;n&&!s.allowIn?.(e)||t==="keydown"&&s.repeat===!1&&e.repeat||s.when&&s.when(e)===!1||o.push(f)}if(!o.length)return;const l=T(o),a=l.options;a.preventDefault&&e.preventDefault(),a.stopImmediatePropagation?e.stopImmediatePropagation():a.stopPropagation&&e.stopPropagation(),l.handler(e),a.once&&m(l.id)}}let y=null,w=null;function x({target:t=h,capture:e=!1}={}){if(!t)throw new Error("hotkeys.init() requires a target (e.g. window)");(y||w)&&A(),y=E(t,"keydown",S("keydown"),{capture:e}),w=E(t,"keyup",S("keyup"),{capture:e})}function A(){y?.(),w?.(),y=null,w=null,u.clear(),d.clear(),p.clear()}function X(t,{type:e}={}){const{combo:n,type:r}=P(t),i=g(n),o=e||r,l=u.get(i)?.get(o);if(!l?.length)return!1;const a={type:o,key:n.key,ctrlKey:!!n.ctrl,shiftKey:!!n.shift,altKey:!!n.alt,metaKey:!!n.meta,repeat:!1,preventDefault(){},stopPropagation(){},stopImmediatePropagation(){},target:null},f=T(l.filter(s=>s.options.when?s.options.when(a)!==!1:!0));return f?(f.handler(a),f.options.once&&m(f.id),!0):!1}h&&x();var G={init:x,destroy:A,setTarget:R,bind:K,unbind:j,registerPlugin:N,unregisterPlugin:M,pause:W,resume:q,ignoreInput:H,trigger:X};export{G as default};
3
+ function P(e,t,n,s){return e.addEventListener(t,n,s),()=>e.removeEventListener(t,n,s)}var D=typeof navigator<"u"&&/Mac|iPhone|iPad|iPod/.test(navigator.platform),F=new Set(["ctrl","shift","alt","meta"]),L=Object.freeze({Alt:"alt",AltGraph:"alt",Control:"ctrl",Shift:"shift",Meta:"meta",OS:"meta"}),R=new Map([["^","ctrl"],["!","alt"],["+","shift"],["#","meta"]]),T=Object.freeze({esc:"escape",escape:"escape",space:" ",spacebar:" ",enter:"enter",return:"enter",del:"delete",delete:"delete",backspace:"backspace",tab:"tab",up:"arrowup",down:"arrowdown",left:"arrowleft",right:"arrowright",home:"home",end:"end",pageup:"pageup",pagedown:"pagedown",pgup:"pageup",pgdn:"pagedown",cmd:"meta",command:"meta",win:"meta",meta:"meta",control:"ctrl",ctrl:"ctrl",option:"alt",alt:"alt",shift:"shift","\u2318":"meta","\u2303":"ctrl","\u2325":"alt","\u21E7":"shift",mod:D?"meta":"ctrl",plus:"+"}),S=!0,_=typeof window<"u"?window:null,y=[],g=new Map,w=new Map,m=new Map,K=!1,N=1;function v(e){let t=String(e??"").trim(),n=t.toLowerCase();return T[n]??(t===" "?" ":n)}function E({ctrl:e,shift:t,alt:n,meta:s,key:o,code:r,bareModifier:l}){if(l)return`__bare__:${l}`;let i="";return e&&(i+="ctrl+"),t&&(i+="shift+"),n&&(i+="alt+"),s&&(i+="meta+"),r?i+"code:"+r:i?i+o:o}function U(e){let t=L[e.key];return t?`__bare__:${t}`:E({ctrl:e.ctrlKey,shift:e.shiftKey,alt:e.altKey,meta:e.metaKey,key:v(e.key)})}function Z(e){if(!e.code)return null;let t="";return e.ctrlKey&&(t+="ctrl+"),e.shiftKey&&(t+="shift+"),e.altKey&&(t+="alt+"),e.metaKey&&(t+="meta+"),t+"code:"+e.code}function j(e){let t=v(e);return F.has(t)}var H=/(^|\+)code:([A-Za-z][A-Za-z0-9_]*)/;function I(e){let t=String(e||"").trim(),n=/\s+up$/i.test(t),s=n?t.replace(/\s+up$/i,"").trim():t,o={ctrl:!1,shift:!1,alt:!1,meta:!1,key:null,code:null,bareModifier:null},r=s;for(;r.length;){let a=R.get(r[0])??null;if(!a)break;o[a]=!0,r=r.slice(1).trimStart()}let l=r.match(/code:[A-Za-z][A-Za-z0-9_]*/g);if(l&&l.length>1)throw new Error(`Invalid hotkey "${e}": multiple code: tokens not supported (got ${l.length}).`);let i=null,f=r.replace(H,(a,p,B)=>(i=B,p+"__code__")).toLowerCase(),u=f.split("+").map(a=>a.trim()),h=u.filter(Boolean),d=u.some(a=>a==="")&&f.includes("++");if(!d){let a=!1;for(let p of h)if(j(p)){if(a)throw new Error(`Invalid hotkey "${e}": modifiers must come before the base key (e.g. "ctrl+k", not "k+ctrl")`)}else a||(a=!0)}for(let a of h){if(a==="__code__"){o.code=i;continue}let p=T[a]??a;p==="ctrl"?o.ctrl=!0:p==="shift"?o.shift=!0:p==="alt"?o.alt=!0:p==="meta"?o.meta=!0:o.key=v(p)}if(!o.key&&!o.code&&d&&(o.key="+"),!o.key&&!o.code){let a=(o.ctrl?1:0)+(o.shift?1:0)+(o.alt?1:0)+(o.meta?1:0);if(a===1)o.bareModifier=o.ctrl?"ctrl":o.shift?"shift":o.alt?"alt":"meta";else throw a>1?new Error(`Invalid hotkey "${e}": multi-modifier bare bindings not supported. Add a base key (e.g. "ctrl+alt+x").`):new Error(`Invalid hotkey "${e}" (missing base key)`)}return{combo:o,type:n?"keyup":"keydown",raw:e}}function Y(e){let t=e?.target;if(!t)return!1;let n=String(t.tagName||"").toUpperCase();return t.isContentEditable||n==="INPUT"||n==="TEXTAREA"||n==="SELECT"||t.getAttribute?.("role")==="textbox"}function q(e,t){let n=t?{...t}:{};return e==="keydown"&&n.repeat==null&&(n.repeat=!1),n.priority==null&&(n.priority=0),n}function G(e,t){let n=g.get(e);n||(n=new Map,g.set(e,n));let s=n.get(t);return s||(s=[],n.set(t,s)),s}function M(e){let t=m.get(e);if(!t)return!1;let{comboStr:n,type:s}=t,o=g.get(n),r=o?.get(s);if(!r?.length)return m.delete(e),!1;let l=r.filter(i=>i.id!==e);return l.length?o.set(s,l):o.delete(s),o.size||g.delete(n),m.delete(e),!0}function O(e,t,n=null,s={}){let{combo:o,type:r,raw:l}=I(e),i=E(o),c=q(r,s),f={id:N++,comboStr:i,type:r,combo:o,handler:t,plugin:n,raw:l,options:c};if(G(i,r).push(f),m.set(f.id,{comboStr:i,type:r}),y.length)for(let h of y)try{h({combo:o,raw:l,options:c,plugin:n,id:f.id})}catch(d){console.error("[hotkey-router] onBind hook threw:",d)}let u=()=>M(f.id);return u.id=f.id,u.hotkey=l,u}function V(e){if(typeof e!="function")throw new TypeError("hotkeys.onBind(hook) expects a function");return y.push(e),()=>{y=y.filter(t=>t!==e)}}function W(e,t){let{combo:n,type:s}=I(e),o=E(n),r=g.get(o);if(!r)return;let l=r.get(s);if(l?.length){if(t){let i=[];for(let c of l)c.handler===t?m.delete(c.id):i.push(c);i.length?r.set(s,i):r.delete(s)}else{for(let i of l)m.delete(i.id);r.delete(s)}r.size||g.delete(o)}}function X(e,t){if(w.has(e))throw new Error(`Plugin "${e}" already registered`);let n=[];for(let[s,o]of Object.entries(t)){let[r,l]=Array.isArray(o)?o:[o,{}],i=O(s,r,e,l);n.push(i.id)}return w.set(e,n),()=>x(e)}function x(e){let t=w.get(e);if(t){for(let n of t)M(n);w.delete(e)}}function J(){K=!0}function Q(){K=!1}function ee(e=!0){S=!!e}function te(e){_=e}function z(e){let t=null;for(let n of e)t?(n.options.priority>t.options.priority||n.options.priority===t.options.priority&&n.id>t.id)&&(t=n):t=n;return t}function A(e){return t=>{if(K)return;let n=S&&Y(t),s=U(t),o=Z(t),r=new Set,l=[];for(let f of[s,o]){if(!f||r.has(f))continue;r.add(f);let u=g.get(f)?.get(e);if(u?.length)for(let h of u){let d=h.options;n&&!d.allowIn?.(t)||e==="keydown"&&d.repeat===!1&&t.repeat||d.when&&d.when(t)===!1||l.push(h)}}if(!l.length)return;let i=z(l),c=i.options;c.preventDefault&&t.preventDefault(),c.stopImmediatePropagation?t.stopImmediatePropagation():c.stopPropagation&&t.stopPropagation(),i.handler(t),c.once&&M(i.id)}}var b=null,k=null;function $({target:e=_,capture:t=!1}={}){if(!e)throw new Error("hotkeys.init() requires a target (e.g. window)");(b||k)&&C(),b=P(e,"keydown",A("keydown"),{capture:t}),k=P(e,"keyup",A("keyup"),{capture:t})}function C(){b?.(),k?.(),b=null,k=null,g.clear(),w.clear(),m.clear()}var ne={ctrl:"Control",shift:"Shift",alt:"Alt",meta:"Meta"};function oe(e,{type:t}={}){let{combo:n,type:s}=I(e),o=E(n),r=t||s,l=g.get(o)?.get(r);if(!l?.length)return!1;let i=n.bareModifier?ne[n.bareModifier]:n.key,c=!!n.alt,f=!!n.ctrl,u=!!n.shift,h=!!n.meta;n.bareModifier&&r==="keyup"&&(n.bareModifier==="alt"?c=!1:n.bareModifier==="ctrl"?f=!1:n.bareModifier==="shift"?u=!1:n.bareModifier==="meta"&&(h=!1));let d={type:r,key:i,code:n.code||null,ctrlKey:f,shiftKey:u,altKey:c,metaKey:h,repeat:!1,preventDefault(){},stopPropagation(){},stopImmediatePropagation(){},target:null},a=z(l.filter(p=>p.options.when?p.options.when(d)!==!1:!0));return a?(a.handler(d),a.options.once&&M(a.id),!0):!1}_&&$();var re={init:$,destroy:C,setTarget:te,bind:O,unbind:W,registerPlugin:X,unregisterPlugin:x,onBind:V,pause:J,resume:Q,ignoreInput:ee,trigger:oe};export{re as default};