hotkey-router 0.2.2 → 0.3.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,4 +1,4 @@
1
- // hotkey-router v0.2.2 | SEE LICENSE IN LICENSE
1
+ // hotkey-router v0.3.0 | SEE LICENSE IN LICENSE
2
2
 
3
3
 
4
4
  // hotkey-router.js
@@ -79,9 +79,11 @@ var bindingIndex = /* @__PURE__ */ new Map();
79
79
  var paused = false;
80
80
  var nextId = 1;
81
81
  function normalizeKey(key) {
82
- const raw = String(key ?? "").trim();
83
- const k = raw.toLowerCase();
84
- return keyAliases[k] ?? (raw === " " ? " " : k);
82
+ const original = String(key ?? "");
83
+ if (original === " ") return " ";
84
+ const trimmed = original.trim();
85
+ const k = trimmed.toLowerCase();
86
+ return keyAliases[k] ?? k;
85
87
  }
86
88
  function comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {
87
89
  if (bareModifier) return `__bare__:${bareModifier}`;
@@ -334,9 +336,19 @@ function pickWinner(bindings) {
334
336
  }
335
337
  return best;
336
338
  }
337
- function handleEvent(type) {
339
+ function isAltGraphCharacter(e) {
340
+ return Boolean(
341
+ e.ctrlKey && e.altKey && typeof e.getModifierState === "function" && e.getModifierState("AltGraph") && typeof e.key === "string" && e.key.length === 1
342
+ );
343
+ }
344
+ function isComposingEvent(e) {
345
+ return Boolean(e.isComposing || e.keyCode === 229);
346
+ }
347
+ function handleEvent(type, phase = "all") {
338
348
  return (e) => {
339
349
  if (paused) return;
350
+ const altGraphChar = isAltGraphCharacter(e);
351
+ const composing = isComposingEvent(e);
340
352
  const fromInput = ignoreEditable && isFromInputTarget(e);
341
353
  const comboStr = comboKeyFromEvent(e);
342
354
  const codeStr = codeKeyFromEvent(e);
@@ -349,6 +361,10 @@ function handleEvent(type) {
349
361
  if (!bindings?.length) continue;
350
362
  for (const b of bindings) {
351
363
  const o2 = b.options;
364
+ if (phase === "capture" && !o2.capture) continue;
365
+ if (phase === "bubble" && o2.capture) continue;
366
+ if (altGraphChar && !o2.altGraph) continue;
367
+ if (composing && !o2.composing) continue;
352
368
  if (fromInput && !o2.allowIn?.(e)) continue;
353
369
  if (type === "keydown" && o2.repeat === false && e.repeat) continue;
354
370
  if (o2.when && o2.when(e) === false) continue;
@@ -367,17 +383,30 @@ function handleEvent(type) {
367
383
  }
368
384
  var stopDown = null;
369
385
  var stopUp = null;
386
+ var stopCaptureDown = null;
387
+ var stopCaptureUp = null;
370
388
  function init({ target = defaultTarget, capture = false } = {}) {
371
389
  if (!target) throw new Error("hotkeys.init() requires a target (e.g. window)");
372
390
  if (stopDown || stopUp) destroy();
373
- stopDown = on(target, "keydown", handleEvent("keydown"), { capture });
374
- stopUp = on(target, "keyup", handleEvent("keyup"), { capture });
391
+ if (capture) {
392
+ stopDown = on(target, "keydown", handleEvent("keydown"), { capture: true });
393
+ stopUp = on(target, "keyup", handleEvent("keyup"), { capture: true });
394
+ return;
395
+ }
396
+ stopDown = on(target, "keydown", handleEvent("keydown", "bubble"), { capture: false });
397
+ stopUp = on(target, "keyup", handleEvent("keyup", "bubble"), { capture: false });
398
+ stopCaptureDown = on(target, "keydown", handleEvent("keydown", "capture"), { capture: true });
399
+ stopCaptureUp = on(target, "keyup", handleEvent("keyup", "capture"), { capture: true });
375
400
  }
376
401
  function destroy() {
377
402
  stopDown?.();
378
403
  stopUp?.();
404
+ stopCaptureDown?.();
405
+ stopCaptureUp?.();
379
406
  stopDown = null;
380
407
  stopUp = null;
408
+ stopCaptureDown = null;
409
+ stopCaptureUp = null;
381
410
  registry.clear();
382
411
  plugins.clear();
383
412
  bindingIndex.clear();
@@ -430,6 +459,23 @@ function trigger(hotkeyStr, { type: forcedType } = {}) {
430
459
  if (winner.options.once) removeById(winner.id);
431
460
  return true;
432
461
  }
462
+ function comboFromEvent(e, { physical = true, useMod = false } = {}) {
463
+ if (!e || MODIFIER_KEY_TOKEN[e.key]) return null;
464
+ const parts = [];
465
+ const primaryIsMeta = isMac;
466
+ if (useMod && (primaryIsMeta ? e.metaKey : e.ctrlKey)) parts.push("mod");
467
+ if (e.ctrlKey && !(useMod && !primaryIsMeta)) parts.push("ctrl");
468
+ if (e.metaKey && !(useMod && primaryIsMeta)) parts.push("meta");
469
+ if (e.altKey) parts.push("alt");
470
+ if (e.shiftKey) parts.push("shift");
471
+ if (physical && e.code) parts.push(`code:${e.code}`);
472
+ else {
473
+ const key = normalizeKey(e.key);
474
+ if (!key) return null;
475
+ parts.push(key === " " ? "space" : key);
476
+ }
477
+ return parts.join("+");
478
+ }
433
479
  if (defaultTarget) init();
434
480
  var hotkey_router_default = {
435
481
  // lifecycle
@@ -448,9 +494,14 @@ var hotkey_router_default = {
448
494
  resume,
449
495
  ignoreInput,
450
496
  // testing / automation
451
- trigger
497
+ trigger,
498
+ // helpers
499
+ parseHotkey,
500
+ comboFromEvent
452
501
  };
453
502
  export {
454
- hotkey_router_default as default
503
+ comboFromEvent,
504
+ hotkey_router_default as default,
505
+ parseHotkey
455
506
  };
456
507
  //# sourceMappingURL=hotkey-router.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../hotkey-router.js"],
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// -----------------------------------------------------------------------------\n// Public type surface (consumed by the tsc-from-JSDoc pipeline)\n// -----------------------------------------------------------------------------\n\n/**\n * Handler invoked when a bound hotkey fires. Receives the underlying\n * `KeyboardEvent` (real one from the DOM, or a synthetic one produced by\n * {@link trigger}).\n *\n * @typedef {(e: KeyboardEvent) => void} HotkeyHandler\n */\n\n/**\n * Optional per-binding behavior modifiers. See the inline comment on\n * `normalizeOptions` for the canonical list.\n *\n * @typedef {{\n * preventDefault?: boolean,\n * stopPropagation?: boolean,\n * stopImmediatePropagation?: boolean,\n * repeat?: boolean,\n * once?: boolean,\n * when?: (e: KeyboardEvent) => boolean,\n * allowIn?: (e: KeyboardEvent) => boolean,\n * priority?: number\n * }} BindOptions\n */\n\n/**\n * Return value of {@link bind}: callable to unbind, plus `id` (the unique\n * binding id used internally) and `hotkey` (the original input string).\n *\n * @typedef {(() => boolean) & { id: number, hotkey: string }} BindOff\n */\n\n/**\n * Payload passed to subscribers registered via {@link onBind} after each\n * successful bind.\n *\n * @typedef {{\n * combo: ReturnType<typeof parseHotkey>['combo'],\n * raw: string,\n * options: BindOptions,\n * plugin: string | null,\n * id: number\n * }} BindEvent\n */\n\n/**\n * Plugin shape accepted by {@link registerPlugin}: a map from hotkey\n * strings to either a handler or a `[handler, options]` tuple.\n *\n * @typedef {Record<string, HotkeyHandler | [HotkeyHandler, BindOptions]>} HotkeyMap\n */\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.\n\n/**\n * Bind a hotkey combo string to a handler.\n *\n * @param {string} hotkeyStr e.g. `'ctrl+shift+k'`, `'meta+slash'`, `'alt+code:KeyX'`\n * @param {HotkeyHandler} handler\n * @param {string | null} [plugin] optional plugin/namespace tag for bulk cleanup\n * @param {BindOptions} [options]\n * @returns {BindOff} unbind function with `id` + `hotkey` props\n */\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`.\n\n/**\n * Subscribe to {@link bind} events. The hook is called after each successful\n * bind with a {@link BindEvent} payload.\n *\n * @param {(info: BindEvent) => void} hook\n * @returns {() => void} unsubscribe\n */\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/**\n * Remove bindings for a combo string. Omit `handler` to remove all bindings\n * for the combo/type; pass a specific handler to remove just that one.\n *\n * @param {string} hotkeyStr\n * @param {HotkeyHandler} [handler]\n * @returns {void}\n */\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.\n\n/**\n * Register a named plugin that bulk-binds a {@link HotkeyMap}. All bindings\n * are tagged with the plugin name so {@link unregisterPlugin} can clean them\n * up together without touching other plugins' bindings.\n *\n * @param {string} name plugin name (must be unique)\n * @param {HotkeyMap} hotkeyMap\n * @returns {() => void} unregister fn for this plugin\n */\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\n/**\n * Remove all bindings owned by the named plugin. Silent no-op if no such\n * plugin is registered.\n *\n * @param {string} name\n * @returns {void}\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 ---\n\n/** Pause dispatch \u2014 bindings stay registered but no handlers fire. @returns {void} */\nfunction pause() {\n paused = true\n}\n\n/** Resume dispatch after a {@link pause}. @returns {void} */\nfunction resume() {\n paused = false\n}\n\n/**\n * Toggle whether handlers fire when the event target is an editable element\n * (input/textarea/contenteditable). Default `true`. Override per-binding via\n * `options.allowIn`.\n *\n * @param {boolean} [value]\n * @returns {void}\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.\n\n/**\n * Replace the default target (used by {@link init} when no `target` is\n * provided). Useful for iframes and tests. Does NOT auto-rebind existing\n * listeners \u2014 call {@link destroy} + {@link init} to re-attach.\n *\n * @param {EventTarget | null} target\n * @returns {void}\n */\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\n/**\n * Initialize the router by attaching `keydown`/`keyup` listeners to a target.\n * Re-initializes cleanly if already running (calls {@link destroy} first).\n * Auto-runs on `window` at module-load time in browser environments.\n *\n * @param {{ target?: EventTarget, capture?: boolean }} [opts]\n * @returns {void}\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\n/**\n * Tear down listeners and clear all bindings + plugins. Bind-hook\n * subscribers ({@link onBind}) deliberately survive \u2014 consumer\n * extensions should install once and live across `init`/`destroy` cycles.\n *\n * @returns {void}\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\n/**\n * Programmatically fire any bindings for `hotkeyStr`. Used by tests and\n * automation; not a typical app-runtime path. Returns `true` if a binding\n * was found and invoked, `false` otherwise.\n *\n * @param {string} hotkeyStr\n * @param {{ type?: 'keydown' | 'keyup' }} [opts]\n * @returns {boolean}\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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",
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// -----------------------------------------------------------------------------\n// Public type surface (consumed by the tsc-from-JSDoc pipeline)\n// -----------------------------------------------------------------------------\n\n/**\n * Handler invoked when a bound hotkey fires. Receives the underlying\n * `KeyboardEvent` (real one from the DOM, or a synthetic one produced by\n * {@link trigger}).\n *\n * @typedef {(e: KeyboardEvent) => void} HotkeyHandler\n */\n\n/**\n * Optional per-binding behavior modifiers. See the inline comment on\n * `normalizeOptions` for the canonical list.\n *\n * @typedef {{\n * preventDefault?: boolean,\n * stopPropagation?: boolean,\n * stopImmediatePropagation?: boolean,\n * repeat?: boolean,\n * once?: boolean,\n * when?: (e: KeyboardEvent) => boolean,\n * allowIn?: (e: KeyboardEvent) => boolean,\n * priority?: number,\n * capture?: boolean,\n * altGraph?: boolean,\n * composing?: boolean\n * }} BindOptions\n */\n\n/**\n * Return value of {@link bind}: callable to unbind, plus `id` (the unique\n * binding id used internally) and `hotkey` (the original input string).\n *\n * @typedef {(() => boolean) & { id: number, hotkey: string }} BindOff\n */\n\n/**\n * Payload passed to subscribers registered via {@link onBind} after each\n * successful bind.\n *\n * @typedef {{\n * combo: ReturnType<typeof parseHotkey>['combo'],\n * raw: string,\n * options: BindOptions,\n * plugin: string | null,\n * id: number\n * }} BindEvent\n */\n\n/**\n * Plugin shape accepted by {@link registerPlugin}: a map from hotkey\n * strings to either a handler or a `[handler, options]` tuple.\n *\n * @typedef {Record<string, HotkeyHandler | [HotkeyHandler, BindOptions]>} HotkeyMap\n */\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 original = String(key ?? '')\n // A lone space IS the spacebar key \u2014 `.trim()` would erase it to '', so guard\n // on the ORIGINAL before trimming. (The `space`/`spacebar` aliases resolve to\n // ' ' and land here; without this, `space` bindings collapse to no base key.)\n if (original === ' ') return ' '\n const trimmed = original.trim()\n const k = trimmed.toLowerCase()\n return keyAliases[k] ?? 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)\n// capture (true: handled in the capture phase, before the page's own\n// listeners; see init)\n// altGraph (true: also fire on an AltGr keystroke that types a character;\n// by default those are skipped, since Windows reports AltGr as Ctrl+Alt\n// and binding ctrl+alt+X would swallow the character the user typed)\n// composing (true: also fire during IME composition; skipped by default)\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.\n\n/**\n * Bind a hotkey combo string to a handler.\n *\n * @param {string} hotkeyStr e.g. `'ctrl+shift+k'`, `'meta+slash'`, `'alt+code:KeyX'`\n * @param {HotkeyHandler} handler\n * @param {string | null} [plugin] optional plugin/namespace tag for bulk cleanup\n * @param {BindOptions} [options]\n * @returns {BindOff} unbind function with `id` + `hotkey` props\n */\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`.\n\n/**\n * Subscribe to {@link bind} events. The hook is called after each successful\n * bind with a {@link BindEvent} payload.\n *\n * @param {(info: BindEvent) => void} hook\n * @returns {() => void} unsubscribe\n */\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/**\n * Remove bindings for a combo string. Omit `handler` to remove all bindings\n * for the combo/type; pass a specific handler to remove just that one.\n *\n * @param {string} hotkeyStr\n * @param {HotkeyHandler} [handler]\n * @returns {void}\n */\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.\n\n/**\n * Register a named plugin that bulk-binds a {@link HotkeyMap}. All bindings\n * are tagged with the plugin name so {@link unregisterPlugin} can clean them\n * up together without touching other plugins' bindings.\n *\n * @param {string} name plugin name (must be unique)\n * @param {HotkeyMap} hotkeyMap\n * @returns {() => void} unregister fn for this plugin\n */\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\n/**\n * Remove all bindings owned by the named plugin. Silent no-op if no such\n * plugin is registered.\n *\n * @param {string} name\n * @returns {void}\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 ---\n\n/** Pause dispatch \u2014 bindings stay registered but no handlers fire. @returns {void} */\nfunction pause() {\n paused = true\n}\n\n/** Resume dispatch after a {@link pause}. @returns {void} */\nfunction resume() {\n paused = false\n}\n\n/**\n * Toggle whether handlers fire when the event target is an editable element\n * (input/textarea/contenteditable). Default `true`. Override per-binding via\n * `options.allowIn`.\n *\n * @param {boolean} [value]\n * @returns {void}\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.\n\n/**\n * Replace the default target (used by {@link init} when no `target` is\n * provided). Useful for iframes and tests. Does NOT auto-rebind existing\n * listeners \u2014 call {@link destroy} + {@link init} to re-attach.\n *\n * @param {EventTarget | null} target\n * @returns {void}\n */\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\n// An AltGr keystroke that produces a character: Chrome on Windows reports it\n// as ctrlKey + altKey with getModifierState('AltGraph'), and a one-character\n// `key` (AltGr+, types '<' on Polish, Czech and Canadian layouts).\nfunction isAltGraphCharacter(e) {\n return Boolean(\n e.ctrlKey && e.altKey &&\n typeof e.getModifierState === 'function' && e.getModifierState('AltGraph') &&\n typeof e.key === 'string' && e.key.length === 1\n )\n}\n\nfunction isComposingEvent(e) {\n return Boolean(e.isComposing || e.keyCode === 229)\n}\n\n// phase: 'all' (one listener handles every binding), 'capture' (only\n// bindings with options.capture) or 'bubble' (only bindings without it).\nfunction handleEvent(type, phase = 'all') {\n return (e) => {\n if (paused) return\n\n const altGraphChar = isAltGraphCharacter(e)\n const composing = isComposingEvent(e)\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 (phase === 'capture' && !o.capture) continue\n if (phase === 'bubble' && o.capture) continue\n if (altGraphChar && !o.altGraph) continue\n if (composing && !o.composing) continue\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\nlet stopCaptureDown = null\nlet stopCaptureUp = null\n\n/**\n * Initialize the router by attaching `keydown`/`keyup` listeners to a target.\n * Re-initializes cleanly if already running (calls {@link destroy} first).\n * Auto-runs on `window` at module-load time in browser environments.\n *\n * `capture: true` handles every binding in the capture phase. With the\n * default (`false`), bindings run in the bubble phase except those bound\n * with `{ capture: true }`, which a second, capture-phase listener handles:\n * they see the key before the page's own listeners and can stop it.\n *\n * @param {{ target?: EventTarget, capture?: boolean }} [opts]\n * @returns {void}\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 if (capture) {\n stopDown = on(target, 'keydown', handleEvent('keydown'), { capture: true })\n stopUp = on(target, 'keyup', handleEvent('keyup'), { capture: true })\n return\n }\n stopDown = on(target, 'keydown', handleEvent('keydown', 'bubble'), { capture: false })\n stopUp = on(target, 'keyup', handleEvent('keyup', 'bubble'), { capture: false })\n stopCaptureDown = on(target, 'keydown', handleEvent('keydown', 'capture'), { capture: true })\n stopCaptureUp = on(target, 'keyup', handleEvent('keyup', 'capture'), { capture: true })\n}\n\n\n/**\n * Tear down listeners and clear all bindings + plugins. Bind-hook\n * subscribers ({@link onBind}) deliberately survive \u2014 consumer\n * extensions should install once and live across `init`/`destroy` cycles.\n *\n * @returns {void}\n */\nfunction destroy() {\n stopDown?.()\n stopUp?.()\n stopCaptureDown?.()\n stopCaptureUp?.()\n stopDown = null\n stopUp = null\n stopCaptureDown = null\n stopCaptureUp = 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\n/**\n * Programmatically fire any bindings for `hotkeyStr`. Used by tests and\n * automation; not a typical app-runtime path. Returns `true` if a binding\n * was found and invoked, `false` otherwise.\n *\n * @param {string} hotkeyStr\n * @param {{ type?: 'keydown' | 'keyup' }} [opts]\n * @returns {boolean}\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/**\n * The hotkey string for a keyboard event, for a \"press your keys\" recorder.\n * Physical by default (`ctrl+shift+code:Period`), so the binding survives\n * keyboard layouts. With `useMod`, the platform's primary modifier (Cmd on\n * macOS, Ctrl elsewhere) is written as `mod`. Returns null for a bare\n * modifier press (the user hasn't pressed the base key yet).\n *\n * @param {KeyboardEvent} e\n * @param {{ physical?: boolean, useMod?: boolean }} [opts]\n * @returns {string | null}\n */\nfunction comboFromEvent(e, { physical = true, useMod = false } = {}) {\n if (!e || MODIFIER_KEY_TOKEN[e.key]) return null\n const parts = []\n const primaryIsMeta = isMac\n if (useMod && (primaryIsMeta ? e.metaKey : e.ctrlKey)) parts.push('mod')\n if (e.ctrlKey && !(useMod && !primaryIsMeta)) parts.push('ctrl')\n if (e.metaKey && !(useMod && primaryIsMeta)) parts.push('meta')\n if (e.altKey) parts.push('alt')\n if (e.shiftKey) parts.push('shift')\n if (physical && e.code) parts.push(`code:${e.code}`)\n else {\n const key = normalizeKey(e.key)\n if (!key) return null\n parts.push(key === ' ' ? 'space' : key)\n }\n return parts.join('+')\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\n // helpers\n parseHotkey,\n comboFromEvent,\n}\n\nexport { parseHotkey, comboFromEvent }\n"],
5
+ "mappings": ";;;;AAyEA,SAAS,GAAG,QAAQ,MAAM,IAAI,SAAS;AACrC,SAAO,iBAAiB,MAAM,IAAI,OAAO;AACzC,SAAO,MAAM,OAAO,oBAAoB,MAAM,IAAI,OAAO;AAC3D;AAGA,IAAM,QACJ,OAAO,cAAc,eAAe,uBAAuB,KAAK,UAAU,QAAQ;AAGpF,IAAM,iBAAiB,oBAAI,IAAI,CAAC,QAAQ,SAAS,OAAO,MAAM,CAAC;AAK/D,IAAM,qBAAqB,OAAO,OAAO;AAAA,EACvC,KAAK;AAAA,EACL,UAAU;AAAA,EACV,SAAS;AAAA,EACT,OAAO;AAAA,EACP,MAAM;AAAA,EACN,IAAI;AACN,CAAC;AAED,IAAM,eAAe,oBAAI,IAAI;AAAA,EAC3B,CAAC,KAAK,MAAM;AAAA;AAAA,EACZ,CAAC,KAAK,KAAK;AAAA;AAAA,EACX,CAAC,KAAK,OAAO;AAAA;AAAA,EACb,CAAC,KAAK,MAAM;AAAA;AACd,CAAC;AAGD,IAAM,aAAa,OAAO,OAAO;AAAA;AAAA,EAE/B,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,OAAO;AAAA,EACP,UAAU;AAAA,EACV,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,KAAK;AAAA;AAAA,EAGL,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA;AAAA,EAGP,MAAM;AAAA,EACN,KAAK;AAAA,EACL,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,MAAM;AAAA,EACN,MAAM;AAAA;AAAA,EAGN,KAAK;AAAA,EACL,SAAS;AAAA,EACT,KAAK;AAAA,EACL,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,KAAK;AAAA,EACL,OAAO;AAAA;AAAA,EAGP,UAAK;AAAA,EACL,UAAK;AAAA,EACL,UAAK;AAAA,EACL,UAAK;AAAA;AAAA,EAGL,KAAK,QAAQ,SAAS;AAAA;AAAA,EAGtB,MAAM;AACR,CAAC;AAGD,IAAI,iBAAiB;AACrB,IAAI,gBAAgB,OAAO,WAAW,cAAc,SAAS;AAK7D,IAAI,YAAY,CAAC;AAIjB,IAAM,WAAW,oBAAI,IAAI;AAEzB,IAAM,UAAU,oBAAI,IAAI;AAExB,IAAM,eAAe,oBAAI,IAAI;AAE7B,IAAI,SAAS;AACb,IAAI,SAAS;AAGb,SAAS,aAAa,KAAK;AACzB,QAAM,WAAW,OAAO,OAAO,EAAE;AAIjC,MAAI,aAAa,IAAK,QAAO;AAC7B,QAAM,UAAU,SAAS,KAAK;AAC9B,QAAM,IAAI,QAAQ,YAAY;AAC9B,SAAO,WAAW,CAAC,KAAK;AAC1B;AAEA,SAAS,kBAAkB,EAAE,MAAM,OAAO,KAAK,MAAM,KAAK,MAAM,aAAa,GAAG;AAG9E,MAAI,aAAc,QAAO,YAAY,YAAY;AAEjD,MAAI,IAAI;AACR,MAAI,KAAM,MAAK;AACf,MAAI,MAAO,MAAK;AAChB,MAAI,IAAK,MAAK;AACd,MAAI,KAAM,MAAK;AAGf,MAAI,KAAM,QAAO,IAAI,UAAU;AAC/B,SAAO,IAAI,IAAI,MAAM;AACvB;AAEA,SAAS,kBAAkB,GAAG;AAK5B,QAAM,UAAU,mBAAmB,EAAE,GAAG;AACxC,MAAI,QAAS,QAAO,YAAY,OAAO;AAEvC,SAAO,kBAAkB;AAAA,IACvB,MAAM,EAAE;AAAA,IACR,OAAO,EAAE;AAAA,IACT,KAAK,EAAE;AAAA,IACP,MAAM,EAAE;AAAA,IACR,KAAK,aAAa,EAAE,GAAG;AAAA,EACzB,CAAC;AACH;AAMA,SAAS,iBAAiB,GAAG;AAC3B,MAAI,CAAC,EAAE,KAAM,QAAO;AAEpB,MAAI,IAAI;AACR,MAAI,EAAE,QAAS,MAAK;AACpB,MAAI,EAAE,SAAU,MAAK;AACrB,MAAI,EAAE,OAAQ,MAAK;AACnB,MAAI,EAAE,QAAS,MAAK;AACpB,SAAO,IAAI,UAAU,EAAE;AACzB;AAGA,SAAS,gBAAgB,OAAO;AAC9B,QAAM,IAAI,aAAa,KAAK;AAC5B,SAAO,eAAe,IAAI,CAAC;AAC7B;AAMA,IAAM,gBAAgB;AAEtB,SAAS,YAAY,WAAW;AAC9B,QAAM,MAAM,OAAO,aAAa,EAAE,EAAE,KAAK;AAGzC,QAAM,OAAO,UAAU,KAAK,GAAG;AAC/B,QAAM,UAAU,OAAO,IAAI,QAAQ,WAAW,EAAE,EAAE,KAAK,IAAI;AAc3D,QAAM,QAAQ;AAAA,IACZ,MAAM;AAAA,IAAO,OAAO;AAAA,IAAO,KAAK;AAAA,IAAO,MAAM;AAAA,IAC7C,KAAK;AAAA,IAAM,MAAM;AAAA,IAAM,cAAc;AAAA,EACvC;AAEA,MAAI,WAAW;AACf,SAAO,SAAS,QAAQ;AACtB,UAAM,MAAM,aAAa,IAAI,SAAS,CAAC,CAAC,KAAK;AAC7C,QAAI,CAAC,IAAK;AACV,UAAM,GAAG,IAAI;AACb,eAAW,SAAS,MAAM,CAAC,EAAE,UAAU;AAAA,EACzC;AAIA,QAAM,cAAc,SAAS,MAAM,6BAA6B;AAChE,MAAI,eAAe,YAAY,SAAS,GAAG;AACzC,UAAM,IAAI;AAAA,MACR,mBAAmB,SAAS,+CAA+C,YAAY,MAAM;AAAA,IAC/F;AAAA,EACF;AACA,MAAI,YAAY;AAChB,QAAM,WAAW,SAAS,QAAQ,eAAe,CAAC,GAAG,KAAK,QAAQ;AAChE,gBAAY;AACZ,WAAO,MAAM;AAAA,EACf,CAAC;AAED,MAAI,WAAW,SAAS,YAAY;AAMpC,QAAM,WAAW,SAAS,MAAM,GAAG,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC;AACxD,QAAM,QAAQ,SAAS,OAAO,OAAO;AACrC,QAAM,iBAAiB,SAAS,KAAK,CAAC,MAAM,MAAM,EAAE,KAAK,SAAS,SAAS,IAAI;AAK/E,MAAI,CAAC,gBAAgB;AACnB,QAAI,WAAW;AACf,eAAW,QAAQ,OAAO;AACxB,UAAI,gBAAgB,IAAI,GAAG;AACzB,YAAI,UAAU;AACZ,gBAAM,IAAI;AAAA,YACR,mBAAmB,SAAS;AAAA,UAC9B;AAAA,QACF;AAAA,MACF,OAAO;AACL,YAAI,CAAC,SAAU,YAAW;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAGA,aAAW,QAAQ,OAAO;AACxB,QAAI,SAAS,YAAY;AACvB,YAAM,OAAO;AACb;AAAA,IACF;AACA,UAAM,SAAS,WAAW,IAAI,KAAK;AACnC,QAAI,WAAW,OAAQ,OAAM,OAAO;AAAA,aAC3B,WAAW,QAAS,OAAM,QAAQ;AAAA,aAClC,WAAW,MAAO,OAAM,MAAM;AAAA,aAC9B,WAAW,OAAQ,OAAM,OAAO;AAAA,QACpC,OAAM,MAAM,aAAa,MAAM;AAAA,EACtC;AAEA,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,QAAQ,eAAgB,OAAM,MAAM;AAK7D,MAAI,CAAC,MAAM,OAAO,CAAC,MAAM,MAAM;AAC7B,UAAM,YACH,MAAM,OAAO,IAAI,MAAM,MAAM,QAAQ,IAAI,MACzC,MAAM,MAAM,IAAI,MAAM,MAAM,OAAO,IAAI;AAE1C,QAAI,aAAa,GAAG;AAClB,YAAM,eACJ,MAAM,OAAO,SACb,MAAM,QAAQ,UACd,MAAM,MAAM,QAAQ;AAAA,IACxB,WAAW,WAAW,GAAG;AACvB,YAAM,IAAI;AAAA,QACR,mBAAmB,SAAS;AAAA,MAC9B;AAAA,IACF,OAAO;AACL,YAAM,IAAI,MAAM,mBAAmB,SAAS,sBAAsB;AAAA,IACpE;AAAA,EACF;AAEA,SAAO,EAAE,OAAO,MAAM,OAAO,UAAU,WAAW,KAAK,UAAU;AACnE;AAGA,SAAS,kBAAkB,GAAG;AAC5B,QAAM,IAAI,GAAG;AACb,MAAI,CAAC,EAAG,QAAO;AACf,QAAM,MAAM,OAAO,EAAE,WAAW,EAAE,EAAE,YAAY;AAChD,SACE,EAAE,qBACF,QAAQ,WACR,QAAQ,cACR,QAAQ,YACR,EAAE,eAAe,MAAM,MAAM;AAEjC;AAgBA,SAAS,iBAAiB,MAAM,SAAS;AACvC,QAAM,IAAI,UAAU,EAAE,GAAG,QAAQ,IAAI,CAAC;AACtC,MAAI,SAAS,aAAa,EAAE,UAAU,KAAM,GAAE,SAAS;AACvD,MAAI,EAAE,YAAY,KAAM,GAAE,WAAW;AACrC,SAAO;AACT;AAGA,SAAS,WAAW,UAAU,MAAM;AAClC,MAAI,UAAU,SAAS,IAAI,QAAQ;AACnC,MAAI,CAAC,SAAS;AACZ,cAAU,oBAAI,IAAI;AAClB,aAAS,IAAI,UAAU,OAAO;AAAA,EAChC;AACA,MAAI,MAAM,QAAQ,IAAI,IAAI;AAC1B,MAAI,CAAC,KAAK;AACR,UAAM,CAAC;AACP,YAAQ,IAAI,MAAM,GAAG;AAAA,EACvB;AACA,SAAO;AACT;AAGA,SAAS,WAAW,IAAI;AACtB,QAAM,MAAM,aAAa,IAAI,EAAE;AAC/B,MAAI,CAAC,IAAK,QAAO;AAEjB,QAAM,EAAE,UAAU,KAAK,IAAI;AAC3B,QAAM,UAAU,SAAS,IAAI,QAAQ;AACrC,QAAM,MAAM,SAAS,IAAI,IAAI;AAC7B,MAAI,CAAC,KAAK,QAAQ;AAChB,iBAAa,OAAO,EAAE;AACtB,WAAO;AAAA,EACT;AAEA,QAAM,OAAO,IAAI,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE;AAC1C,MAAI,KAAK,OAAQ,SAAQ,IAAI,MAAM,IAAI;AAAA,MAClC,SAAQ,OAAO,IAAI;AAExB,MAAI,CAAC,QAAQ,KAAM,UAAS,OAAO,QAAQ;AAC3C,eAAa,OAAO,EAAE;AACtB,SAAO;AACT;AAeA,SAAS,KAAK,WAAW,SAAS,SAAS,MAAM,UAAU,CAAC,GAAG;AAC7D,QAAM,EAAE,OAAO,MAAM,IAAI,IAAI,YAAY,SAAS;AAClD,QAAM,WAAW,kBAAkB,KAAK;AACxC,QAAM,OAAO,iBAAiB,MAAM,OAAO;AAE3C,QAAM,UAAU;AAAA,IACd,IAAI;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS;AAAA,EACX;AAEA,aAAW,UAAU,IAAI,EAAE,KAAK,OAAO;AACvC,eAAa,IAAI,QAAQ,IAAI,EAAE,UAAU,KAAK,CAAC;AAG/C,MAAI,UAAU,QAAQ;AACpB,eAAW,QAAQ,WAAW;AAC5B,UAAI;AACF,aAAK,EAAE,OAAO,KAAK,SAAS,MAAM,QAAQ,IAAI,QAAQ,GAAG,CAAC;AAAA,MAC5D,SAAS,KAAK;AAEZ,gBAAQ,MAAM,sCAAsC,GAAG;AAAA,MACzD;AAAA,IACF;AAAA,EACF;AAEA,QAAM,MAAM,MAAM,WAAW,QAAQ,EAAE;AACvC,MAAI,KAAK,QAAQ;AACjB,MAAI,SAAS;AACb,SAAO;AACT;AAcA,SAAS,OAAO,MAAM;AACpB,MAAI,OAAO,SAAS,YAAY;AAC9B,UAAM,IAAI,UAAU,yCAAyC;AAAA,EAC/D;AACA,YAAU,KAAK,IAAI;AACnB,SAAO,MAAM;AACX,gBAAY,UAAU,OAAO,CAAC,MAAM,MAAM,IAAI;AAAA,EAChD;AACF;AAWA,SAAS,OAAO,WAAW,SAAS;AAClC,QAAM,EAAE,OAAO,KAAK,IAAI,YAAY,SAAS;AAC7C,QAAM,WAAW,kBAAkB,KAAK;AACxC,QAAM,UAAU,SAAS,IAAI,QAAQ;AACrC,MAAI,CAAC,QAAS;AAEd,QAAM,MAAM,QAAQ,IAAI,IAAI;AAC5B,MAAI,CAAC,KAAK,OAAQ;AAElB,MAAI,CAAC,SAAS;AACZ,eAAW,KAAK,IAAK,cAAa,OAAO,EAAE,EAAE;AAC7C,YAAQ,OAAO,IAAI;AAAA,EACrB,OAAO;AACL,UAAM,OAAO,CAAC;AACd,eAAW,KAAK,KAAK;AACnB,UAAI,EAAE,YAAY,QAAS,cAAa,OAAO,EAAE,EAAE;AAAA,UAC9C,MAAK,KAAK,CAAC;AAAA,IAClB;AACA,QAAI,KAAK,OAAQ,SAAQ,IAAI,MAAM,IAAI;AAAA,QAClC,SAAQ,OAAO,IAAI;AAAA,EAC1B;AAEA,MAAI,CAAC,QAAQ,KAAM,UAAS,OAAO,QAAQ;AAC7C;AAeA,SAAS,eAAe,MAAM,WAAW;AACvC,MAAI,QAAQ,IAAI,IAAI,EAAG,OAAM,IAAI,MAAM,WAAW,IAAI,sBAAsB;AAC5E,QAAM,MAAM,CAAC;AAEb,aAAW,CAAC,WAAW,SAAS,KAAK,OAAO,QAAQ,SAAS,GAAG;AAC9D,UAAM,CAAC,IAAI,OAAO,IAAI,MAAM,QAAQ,SAAS,IAAI,YAAY,CAAC,WAAW,CAAC,CAAC;AAC3E,UAAM,MAAM,KAAK,WAAW,IAAI,MAAM,OAAO;AAC7C,QAAI,KAAK,IAAI,EAAE;AAAA,EACjB;AAEA,UAAQ,IAAI,MAAM,GAAG;AACrB,SAAO,MAAM,iBAAiB,IAAI;AACpC;AASA,SAAS,iBAAiB,MAAM;AAC9B,QAAM,MAAM,QAAQ,IAAI,IAAI;AAC5B,MAAI,CAAC,IAAK;AACV,aAAW,MAAM,IAAK,YAAW,EAAE;AACnC,UAAQ,OAAO,IAAI;AACrB;AAKA,SAAS,QAAQ;AACf,WAAS;AACX;AAGA,SAAS,SAAS;AAChB,WAAS;AACX;AAUA,SAAS,YAAY,QAAQ,MAAM;AACjC,mBAAiB,CAAC,CAAC;AACrB;AAaA,SAAS,UAAU,QAAQ;AACzB,kBAAgB;AAClB;AAIA,SAAS,WAAW,UAAU;AAE5B,MAAI,OAAO;AACX,aAAW,KAAK,UAAU;AACxB,QAAI,CAAC,KAAM,QAAO;AAAA,aACT,EAAE,QAAQ,WAAW,KAAK,QAAQ,SAAU,QAAO;AAAA,aACnD,EAAE,QAAQ,aAAa,KAAK,QAAQ,YAAY,EAAE,KAAK,KAAK,GAAI,QAAO;AAAA,EAClF;AACA,SAAO;AACT;AAMA,SAAS,oBAAoB,GAAG;AAC9B,SAAO;AAAA,IACL,EAAE,WAAW,EAAE,UACf,OAAO,EAAE,qBAAqB,cAAc,EAAE,iBAAiB,UAAU,KACzE,OAAO,EAAE,QAAQ,YAAY,EAAE,IAAI,WAAW;AAAA,EAChD;AACF;AAEA,SAAS,iBAAiB,GAAG;AAC3B,SAAO,QAAQ,EAAE,eAAe,EAAE,YAAY,GAAG;AACnD;AAIA,SAAS,YAAY,MAAM,QAAQ,OAAO;AACxC,SAAO,CAAC,MAAM;AACZ,QAAI,OAAQ;AAEZ,UAAM,eAAe,oBAAoB,CAAC;AAC1C,UAAM,YAAY,iBAAiB,CAAC;AACpC,UAAM,YAAY,kBAAkB,kBAAkB,CAAC;AACvD,UAAM,WAAW,kBAAkB,CAAC;AACpC,UAAM,UAAU,iBAAiB,CAAC;AAKlC,UAAM,OAAO,oBAAI,IAAI;AACrB,UAAM,aAAa,CAAC;AACpB,eAAW,aAAa,CAAC,UAAU,OAAO,GAAG;AAC3C,UAAI,CAAC,aAAa,KAAK,IAAI,SAAS,EAAG;AACvC,WAAK,IAAI,SAAS;AAElB,YAAM,WAAW,SAAS,IAAI,SAAS,GAAG,IAAI,IAAI;AAClD,UAAI,CAAC,UAAU,OAAQ;AAEvB,iBAAW,KAAK,UAAU;AACxB,cAAMA,KAAI,EAAE;AACZ,YAAI,UAAU,aAAa,CAACA,GAAE,QAAS;AACvC,YAAI,UAAU,YAAYA,GAAE,QAAS;AACrC,YAAI,gBAAgB,CAACA,GAAE,SAAU;AACjC,YAAI,aAAa,CAACA,GAAE,UAAW;AAC/B,YAAI,aAAa,CAACA,GAAE,UAAU,CAAC,EAAG;AAClC,YAAI,SAAS,aAAaA,GAAE,WAAW,SAAS,EAAE,OAAQ;AAC1D,YAAIA,GAAE,QAAQA,GAAE,KAAK,CAAC,MAAM,MAAO;AACnC,mBAAW,KAAK,CAAC;AAAA,MACnB;AAAA,IACF;AACA,QAAI,CAAC,WAAW,OAAQ;AAExB,UAAM,SAAS,WAAW,UAAU;AACpC,UAAM,IAAI,OAAO;AAEjB,QAAI,EAAE,eAAgB,GAAE,eAAe;AACvC,QAAI,EAAE,yBAA0B,GAAE,yBAAyB;AAAA,aAClD,EAAE,gBAAiB,GAAE,gBAAgB;AAE9C,WAAO,QAAQ,CAAC;AAEhB,QAAI,EAAE,KAAM,YAAW,OAAO,EAAE;AAAA,EAClC;AACF;AAIA,IAAI,WAAW;AACf,IAAI,SAAS;AAEb,IAAI,kBAAkB;AACtB,IAAI,gBAAgB;AAepB,SAAS,KAAK,EAAE,SAAS,eAAe,UAAU,MAAM,IAAI,CAAC,GAAG;AAC9D,MAAI,CAAC,OAAQ,OAAM,IAAI,MAAM,gDAAgD;AAC7E,MAAI,YAAY,OAAQ,SAAQ;AAEhC,MAAI,SAAS;AACX,eAAW,GAAG,QAAQ,WAAW,YAAY,SAAS,GAAG,EAAE,SAAS,KAAK,CAAC;AAC1E,aAAS,GAAG,QAAQ,SAAS,YAAY,OAAO,GAAG,EAAE,SAAS,KAAK,CAAC;AACpE;AAAA,EACF;AACA,aAAW,GAAG,QAAQ,WAAW,YAAY,WAAW,QAAQ,GAAG,EAAE,SAAS,MAAM,CAAC;AACrF,WAAS,GAAG,QAAQ,SAAS,YAAY,SAAS,QAAQ,GAAG,EAAE,SAAS,MAAM,CAAC;AAC/E,oBAAkB,GAAG,QAAQ,WAAW,YAAY,WAAW,SAAS,GAAG,EAAE,SAAS,KAAK,CAAC;AAC5F,kBAAgB,GAAG,QAAQ,SAAS,YAAY,SAAS,SAAS,GAAG,EAAE,SAAS,KAAK,CAAC;AACxF;AAUA,SAAS,UAAU;AACjB,aAAW;AACX,WAAS;AACT,oBAAkB;AAClB,kBAAgB;AAChB,aAAW;AACX,WAAS;AACT,oBAAkB;AAClB,kBAAgB;AAChB,WAAS,MAAM;AACf,UAAQ,MAAM;AACd,eAAa,MAAM;AAKrB;AAQA,IAAM,kBAAkB;AAAA,EACtB,MAAM;AAAA,EACN,OAAO;AAAA,EACP,KAAK;AAAA,EACL,MAAM;AACR;AAWA,SAAS,QAAQ,WAAW,EAAE,MAAM,WAAW,IAAI,CAAC,GAAG;AACrD,QAAM,EAAE,OAAO,KAAK,IAAI,YAAY,SAAS;AAC7C,QAAM,WAAW,kBAAkB,KAAK;AACxC,QAAM,aAAa,cAAc;AAEjC,QAAM,WAAW,SAAS,IAAI,QAAQ,GAAG,IAAI,UAAU;AACvD,MAAI,CAAC,UAAU,OAAQ,QAAO;AAI9B,QAAM,WAAW,MAAM,eACnB,gBAAgB,MAAM,YAAY,IAClC,MAAM;AAMV,MAAI,SAAS,CAAC,CAAC,MAAM;AACrB,MAAI,UAAU,CAAC,CAAC,MAAM;AACtB,MAAI,WAAW,CAAC,CAAC,MAAM;AACvB,MAAI,UAAU,CAAC,CAAC,MAAM;AACtB,MAAI,MAAM,gBAAgB,eAAe,SAAS;AAChD,QAAI,MAAM,iBAAiB,MAAO,UAAS;AAAA,aAClC,MAAM,iBAAiB,OAAQ,WAAU;AAAA,aACzC,MAAM,iBAAiB,QAAS,YAAW;AAAA,aAC3C,MAAM,iBAAiB,OAAQ,WAAU;AAAA,EACpD;AAEA,QAAM,IAAI;AAAA,IACR,MAAM;AAAA,IACN,KAAK;AAAA,IACL,MAAM,MAAM,QAAQ;AAAA,IACpB;AAAA,IAAS;AAAA,IAAU;AAAA,IAAQ;AAAA,IAC3B,QAAQ;AAAA,IACR,iBAAiB;AAAA,IAAE;AAAA,IACnB,kBAAkB;AAAA,IAAE;AAAA,IACpB,2BAA2B;AAAA,IAAE;AAAA,IAC7B,QAAQ;AAAA,EACV;AAEA,QAAM,SAAS;AAAA,IACb,SAAS,OAAO,CAAC,MAAO,EAAE,QAAQ,OAAO,EAAE,QAAQ,KAAK,CAAC,MAAM,QAAQ,IAAK;AAAA,EAC9E;AACA,MAAI,CAAC,OAAQ,QAAO;AAEpB,SAAO,QAAQ,CAAC;AAChB,MAAI,OAAO,QAAQ,KAAM,YAAW,OAAO,EAAE;AAC7C,SAAO;AACT;AAcA,SAAS,eAAe,GAAG,EAAE,WAAW,MAAM,SAAS,MAAM,IAAI,CAAC,GAAG;AACnE,MAAI,CAAC,KAAK,mBAAmB,EAAE,GAAG,EAAG,QAAO;AAC5C,QAAM,QAAQ,CAAC;AACf,QAAM,gBAAgB;AACtB,MAAI,WAAW,gBAAgB,EAAE,UAAU,EAAE,SAAU,OAAM,KAAK,KAAK;AACvE,MAAI,EAAE,WAAW,EAAE,UAAU,CAAC,eAAgB,OAAM,KAAK,MAAM;AAC/D,MAAI,EAAE,WAAW,EAAE,UAAU,eAAgB,OAAM,KAAK,MAAM;AAC9D,MAAI,EAAE,OAAQ,OAAM,KAAK,KAAK;AAC9B,MAAI,EAAE,SAAU,OAAM,KAAK,OAAO;AAClC,MAAI,YAAY,EAAE,KAAM,OAAM,KAAK,QAAQ,EAAE,IAAI,EAAE;AAAA,OAC9C;AACH,UAAM,MAAM,aAAa,EAAE,GAAG;AAC9B,QAAI,CAAC,IAAK,QAAO;AACjB,UAAM,KAAK,QAAQ,MAAM,UAAU,GAAG;AAAA,EACxC;AACA,SAAO,MAAM,KAAK,GAAG;AACvB;AAGA,IAAI,cAAe,MAAK;AAExB,IAAO,wBAAQ;AAAA;AAAA,EAEb;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAGA;AAAA;AAAA,EAGA;AAAA,EACA;AACF;",
6
6
  "names": ["o"]
7
7
  }
@@ -1,3 +1,3 @@
1
- // hotkey-router v0.2.2 | SEE LICENSE IN LICENSE
1
+ // hotkey-router v0.3.0 | SEE LICENSE IN LICENSE
2
2
 
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};
3
+ function b(e,n,t,r){return e.addEventListener(n,t,r),()=>e.removeEventListener(n,t,r)}var O=typeof navigator<"u"&&/Mac|iPhone|iPad|iPod/.test(navigator.platform),R=new Set(["ctrl","shift","alt","meta"]),x=Object.freeze({Alt:"alt",AltGraph:"alt",Control:"ctrl",Shift:"shift",Meta:"meta",OS:"meta"}),U=new Map([["^","ctrl"],["!","alt"],["+","shift"],["#","meta"]]),$=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:O?"meta":"ctrl",plus:"+"}),z=!0,S=typeof window<"u"?window:null,k=[],m=new Map,M=new Map,g=new Map,T=!1,j=1;function _(e){let n=String(e??"");if(n===" ")return" ";let r=n.trim().toLowerCase();return $[r]??r}function v({ctrl:e,shift:n,alt:t,meta:r,key:o,code:i,bareModifier:a}){if(a)return`__bare__:${a}`;let s="";return e&&(s+="ctrl+"),n&&(s+="shift+"),t&&(s+="alt+"),r&&(s+="meta+"),i?s+"code:"+i:s?s+o:o}function N(e){let n=x[e.key];return n?`__bare__:${n}`:v({ctrl:e.ctrlKey,shift:e.shiftKey,alt:e.altKey,meta:e.metaKey,key:_(e.key)})}function Z(e){if(!e.code)return null;let n="";return e.ctrlKey&&(n+="ctrl+"),e.shiftKey&&(n+="shift+"),e.altKey&&(n+="alt+"),e.metaKey&&(n+="meta+"),n+"code:"+e.code}function H(e){let n=_(e);return R.has(n)}var Y=/(^|\+)code:([A-Za-z][A-Za-z0-9_]*)/;function I(e){let n=String(e||"").trim(),t=/\s+up$/i.test(n),r=t?n.replace(/\s+up$/i,"").trim():n,o={ctrl:!1,shift:!1,alt:!1,meta:!1,key:null,code:null,bareModifier:null},i=r;for(;i.length;){let l=U.get(i[0])??null;if(!l)break;o[l]=!0,i=i.slice(1).trimStart()}let a=i.match(/code:[A-Za-z][A-Za-z0-9_]*/g);if(a&&a.length>1)throw new Error(`Invalid hotkey "${e}": multiple code: tokens not supported (got ${a.length}).`);let s=null,c=i.replace(Y,(l,f,h)=>(s=h,f+"__code__")).toLowerCase(),p=c.split("+").map(l=>l.trim()),d=p.filter(Boolean),y=p.some(l=>l==="")&&c.includes("++");if(!y){let l=!1;for(let f of d)if(H(f)){if(l)throw new Error(`Invalid hotkey "${e}": modifiers must come before the base key (e.g. "ctrl+k", not "k+ctrl")`)}else l||(l=!0)}for(let l of d){if(l==="__code__"){o.code=s;continue}let f=$[l]??l;f==="ctrl"?o.ctrl=!0:f==="shift"?o.shift=!0:f==="alt"?o.alt=!0:f==="meta"?o.meta=!0:o.key=_(f)}if(!o.key&&!o.code&&y&&(o.key="+"),!o.key&&!o.code){let l=(o.ctrl?1:0)+(o.shift?1:0)+(o.alt?1:0)+(o.meta?1:0);if(l===1)o.bareModifier=o.ctrl?"ctrl":o.shift?"shift":o.alt?"alt":"meta";else throw l>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:t?"keyup":"keydown",raw:e}}function q(e){let n=e?.target;if(!n)return!1;let t=String(n.tagName||"").toUpperCase();return n.isContentEditable||t==="INPUT"||t==="TEXTAREA"||t==="SELECT"||n.getAttribute?.("role")==="textbox"}function V(e,n){let t=n?{...n}:{};return e==="keydown"&&t.repeat==null&&(t.repeat=!1),t.priority==null&&(t.priority=0),t}function W(e,n){let t=m.get(e);t||(t=new Map,m.set(e,t));let r=t.get(n);return r||(r=[],t.set(n,r)),r}function A(e){let n=g.get(e);if(!n)return!1;let{comboStr:t,type:r}=n,o=m.get(t),i=o?.get(r);if(!i?.length)return g.delete(e),!1;let a=i.filter(s=>s.id!==e);return a.length?o.set(r,a):o.delete(r),o.size||m.delete(t),g.delete(e),!0}function B(e,n,t=null,r={}){let{combo:o,type:i,raw:a}=I(e),s=v(o),u=V(i,r),c={id:j++,comboStr:s,type:i,combo:o,handler:n,plugin:t,raw:a,options:u};if(W(s,i).push(c),g.set(c.id,{comboStr:s,type:i}),k.length)for(let d of k)try{d({combo:o,raw:a,options:u,plugin:t,id:c.id})}catch(y){console.error("[hotkey-router] onBind hook threw:",y)}let p=()=>A(c.id);return p.id=c.id,p.hotkey=a,p}function X(e){if(typeof e!="function")throw new TypeError("hotkeys.onBind(hook) expects a function");return k.push(e),()=>{k=k.filter(n=>n!==e)}}function J(e,n){let{combo:t,type:r}=I(e),o=v(t),i=m.get(o);if(!i)return;let a=i.get(r);if(a?.length){if(n){let s=[];for(let u of a)u.handler===n?g.delete(u.id):s.push(u);s.length?i.set(r,s):i.delete(r)}else{for(let s of a)g.delete(s.id);i.delete(r)}i.size||m.delete(o)}}function Q(e,n){if(M.has(e))throw new Error(`Plugin "${e}" already registered`);let t=[];for(let[r,o]of Object.entries(n)){let[i,a]=Array.isArray(o)?o:[o,{}],s=B(r,i,e,a);t.push(s.id)}return M.set(e,t),()=>D(e)}function D(e){let n=M.get(e);if(n){for(let t of n)A(t);M.delete(e)}}function ee(){T=!0}function te(){T=!1}function ne(e=!0){z=!!e}function oe(e){S=e}function F(e){let n=null;for(let t of e)n?(t.options.priority>n.options.priority||t.options.priority===n.options.priority&&t.id>n.id)&&(n=t):n=t;return n}function re(e){return!!(e.ctrlKey&&e.altKey&&typeof e.getModifierState=="function"&&e.getModifierState("AltGraph")&&typeof e.key=="string"&&e.key.length===1)}function ie(e){return!!(e.isComposing||e.keyCode===229)}function w(e,n="all"){return t=>{if(T)return;let r=re(t),o=ie(t),i=z&&q(t),a=N(t),s=Z(t),u=new Set,c=[];for(let y of[a,s]){if(!y||u.has(y))continue;u.add(y);let l=m.get(y)?.get(e);if(l?.length)for(let f of l){let h=f.options;n==="capture"&&!h.capture||n==="bubble"&&h.capture||r&&!h.altGraph||o&&!h.composing||i&&!h.allowIn?.(t)||e==="keydown"&&h.repeat===!1&&t.repeat||h.when&&h.when(t)===!1||c.push(f)}}if(!c.length)return;let p=F(c),d=p.options;d.preventDefault&&t.preventDefault(),d.stopImmediatePropagation?t.stopImmediatePropagation():d.stopPropagation&&t.stopPropagation(),p.handler(t),d.once&&A(p.id)}}var K=null,E=null,P=null,C=null;function G({target:e=S,capture:n=!1}={}){if(!e)throw new Error("hotkeys.init() requires a target (e.g. window)");if((K||E)&&L(),n){K=b(e,"keydown",w("keydown"),{capture:!0}),E=b(e,"keyup",w("keyup"),{capture:!0});return}K=b(e,"keydown",w("keydown","bubble"),{capture:!1}),E=b(e,"keyup",w("keyup","bubble"),{capture:!1}),P=b(e,"keydown",w("keydown","capture"),{capture:!0}),C=b(e,"keyup",w("keyup","capture"),{capture:!0})}function L(){K?.(),E?.(),P?.(),C?.(),K=null,E=null,P=null,C=null,m.clear(),M.clear(),g.clear()}var se={ctrl:"Control",shift:"Shift",alt:"Alt",meta:"Meta"};function le(e,{type:n}={}){let{combo:t,type:r}=I(e),o=v(t),i=n||r,a=m.get(o)?.get(i);if(!a?.length)return!1;let s=t.bareModifier?se[t.bareModifier]:t.key,u=!!t.alt,c=!!t.ctrl,p=!!t.shift,d=!!t.meta;t.bareModifier&&i==="keyup"&&(t.bareModifier==="alt"?u=!1:t.bareModifier==="ctrl"?c=!1:t.bareModifier==="shift"?p=!1:t.bareModifier==="meta"&&(d=!1));let y={type:i,key:s,code:t.code||null,ctrlKey:c,shiftKey:p,altKey:u,metaKey:d,repeat:!1,preventDefault(){},stopPropagation(){},stopImmediatePropagation(){},target:null},l=F(a.filter(f=>f.options.when?f.options.when(y)!==!1:!0));return l?(l.handler(y),l.options.once&&A(l.id),!0):!1}function ae(e,{physical:n=!0,useMod:t=!1}={}){if(!e||x[e.key])return null;let r=[],o=O;if(t&&(o?e.metaKey:e.ctrlKey)&&r.push("mod"),e.ctrlKey&&!(t&&!o)&&r.push("ctrl"),e.metaKey&&!(t&&o)&&r.push("meta"),e.altKey&&r.push("alt"),e.shiftKey&&r.push("shift"),n&&e.code)r.push(`code:${e.code}`);else{let i=_(e.key);if(!i)return null;r.push(i===" "?"space":i)}return r.join("+")}S&&G();var ce={init:G,destroy:L,setTarget:oe,bind:B,unbind:J,registerPlugin:Q,unregisterPlugin:D,onBind:X,pause:ee,resume:te,ignoreInput:ne,trigger:le,parseHotkey:I,comboFromEvent:ae};export{ae as comboFromEvent,ce as default,I as parseHotkey};
@@ -1,4 +1,4 @@
1
- // hotkey-router v0.2.2 | SEE LICENSE IN LICENSE
1
+ // hotkey-router v0.3.0 | SEE LICENSE IN LICENSE
2
2
 
3
3
 
4
4
  // data/browser-hotkeys.json