hotkey-router 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +284 -284
- package/README.md +370 -331
- package/dist/hotkey-router.cjs +105 -24
- package/dist/hotkey-router.cjs.map +2 -2
- package/dist/hotkey-router.js +105 -24
- package/dist/hotkey-router.js.map +2 -2
- package/dist/hotkey-router.min.js +2 -2
- package/package.json +52 -58
package/dist/hotkey-router.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// hotkey-router v0.0
|
|
1
|
+
// hotkey-router v0.1.0 | SEE LICENSE IN LICENSE
|
|
2
2
|
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -28,6 +28,14 @@ function on(target, type, fn, options) {
|
|
|
28
28
|
}
|
|
29
29
|
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
30
30
|
const modifierTokens = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "meta"]);
|
|
31
|
+
const MODIFIER_KEY_TOKEN = Object.freeze({
|
|
32
|
+
Alt: "alt",
|
|
33
|
+
AltGraph: "alt",
|
|
34
|
+
Control: "ctrl",
|
|
35
|
+
Shift: "shift",
|
|
36
|
+
Meta: "meta",
|
|
37
|
+
OS: "meta"
|
|
38
|
+
});
|
|
31
39
|
const ahkPrefixMap = /* @__PURE__ */ new Map([
|
|
32
40
|
["^", "ctrl"],
|
|
33
41
|
// Ctrl
|
|
@@ -94,15 +102,19 @@ function normalizeKey(key) {
|
|
|
94
102
|
const k = raw.toLowerCase();
|
|
95
103
|
return keyAliases[k] ?? (raw === " " ? " " : k);
|
|
96
104
|
}
|
|
97
|
-
function comboKeyFromParts({ ctrl, shift, alt, meta, key }) {
|
|
105
|
+
function comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {
|
|
106
|
+
if (bareModifier) return `__bare__:${bareModifier}`;
|
|
98
107
|
let s = "";
|
|
99
108
|
if (ctrl) s += "ctrl+";
|
|
100
109
|
if (shift) s += "shift+";
|
|
101
110
|
if (alt) s += "alt+";
|
|
102
111
|
if (meta) s += "meta+";
|
|
112
|
+
if (code) return s + "code:" + code;
|
|
103
113
|
return s ? s + key : key;
|
|
104
114
|
}
|
|
105
115
|
function comboKeyFromEvent(e) {
|
|
116
|
+
const bareMod = MODIFIER_KEY_TOKEN[e.key];
|
|
117
|
+
if (bareMod) return `__bare__:${bareMod}`;
|
|
106
118
|
return comboKeyFromParts({
|
|
107
119
|
ctrl: e.ctrlKey,
|
|
108
120
|
shift: e.shiftKey,
|
|
@@ -111,22 +123,52 @@ function comboKeyFromEvent(e) {
|
|
|
111
123
|
key: normalizeKey(e.key)
|
|
112
124
|
});
|
|
113
125
|
}
|
|
126
|
+
function codeKeyFromEvent(e) {
|
|
127
|
+
if (!e.code) return null;
|
|
128
|
+
let s = "";
|
|
129
|
+
if (e.ctrlKey) s += "ctrl+";
|
|
130
|
+
if (e.shiftKey) s += "shift+";
|
|
131
|
+
if (e.altKey) s += "alt+";
|
|
132
|
+
if (e.metaKey) s += "meta+";
|
|
133
|
+
return s + "code:" + e.code;
|
|
134
|
+
}
|
|
114
135
|
function isModifierToken(token) {
|
|
115
136
|
const t = normalizeKey(token);
|
|
116
137
|
return modifierTokens.has(t);
|
|
117
138
|
}
|
|
139
|
+
const CODE_TOKEN_RE = /(^|\+)code:([A-Za-z][A-Za-z0-9_]*)/;
|
|
118
140
|
function parseHotkey(hotkeyStr) {
|
|
119
141
|
const raw = String(hotkeyStr || "").trim();
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
142
|
+
const isUp = /\s+up$/i.test(raw);
|
|
143
|
+
const baseRaw = isUp ? raw.replace(/\s+up$/i, "").trim() : raw;
|
|
144
|
+
const combo = {
|
|
145
|
+
ctrl: false,
|
|
146
|
+
shift: false,
|
|
147
|
+
alt: false,
|
|
148
|
+
meta: false,
|
|
149
|
+
key: null,
|
|
150
|
+
code: null,
|
|
151
|
+
bareModifier: null
|
|
152
|
+
};
|
|
153
|
+
let stripped = baseRaw;
|
|
154
|
+
while (stripped.length) {
|
|
155
|
+
const mod = ahkPrefixMap.get(stripped[0]) ?? null;
|
|
126
156
|
if (!mod) break;
|
|
127
157
|
combo[mod] = true;
|
|
128
|
-
|
|
158
|
+
stripped = stripped.slice(1).trimStart();
|
|
159
|
+
}
|
|
160
|
+
const codeMatches = stripped.match(/code:[A-Za-z][A-Za-z0-9_]*/g);
|
|
161
|
+
if (codeMatches && codeMatches.length > 1) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`Invalid hotkey "${hotkeyStr}": multiple code: tokens not supported (got ${codeMatches.length}).`
|
|
164
|
+
);
|
|
129
165
|
}
|
|
166
|
+
let codeValue = null;
|
|
167
|
+
const scrubbed = stripped.replace(CODE_TOKEN_RE, (_, sep, val) => {
|
|
168
|
+
codeValue = val;
|
|
169
|
+
return sep + "__code__";
|
|
170
|
+
});
|
|
171
|
+
let cleanStr = scrubbed.toLowerCase();
|
|
130
172
|
const rawParts = cleanStr.split("+").map((p) => p.trim());
|
|
131
173
|
const parts = rawParts.filter(Boolean);
|
|
132
174
|
const impliedPlusKey = rawParts.some((p) => p === "") && cleanStr.includes("++");
|
|
@@ -145,6 +187,10 @@ function parseHotkey(hotkeyStr) {
|
|
|
145
187
|
}
|
|
146
188
|
}
|
|
147
189
|
for (const part of parts) {
|
|
190
|
+
if (part === "__code__") {
|
|
191
|
+
combo.code = codeValue;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
148
194
|
const actual = keyAliases[part] ?? part;
|
|
149
195
|
if (actual === "ctrl") combo.ctrl = true;
|
|
150
196
|
else if (actual === "shift") combo.shift = true;
|
|
@@ -152,8 +198,19 @@ function parseHotkey(hotkeyStr) {
|
|
|
152
198
|
else if (actual === "meta") combo.meta = true;
|
|
153
199
|
else combo.key = normalizeKey(actual);
|
|
154
200
|
}
|
|
155
|
-
if (!combo.key && impliedPlusKey) combo.key = "+";
|
|
156
|
-
if (!combo.key
|
|
201
|
+
if (!combo.key && !combo.code && impliedPlusKey) combo.key = "+";
|
|
202
|
+
if (!combo.key && !combo.code) {
|
|
203
|
+
const modCount = (combo.ctrl ? 1 : 0) + (combo.shift ? 1 : 0) + (combo.alt ? 1 : 0) + (combo.meta ? 1 : 0);
|
|
204
|
+
if (modCount === 1) {
|
|
205
|
+
combo.bareModifier = combo.ctrl ? "ctrl" : combo.shift ? "shift" : combo.alt ? "alt" : "meta";
|
|
206
|
+
} else if (modCount > 1) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`Invalid hotkey "${hotkeyStr}": multi-modifier bare bindings not supported. Add a base key (e.g. "ctrl+alt+x").`
|
|
209
|
+
);
|
|
210
|
+
} else {
|
|
211
|
+
throw new Error(`Invalid hotkey "${hotkeyStr}" (missing base key)`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
157
214
|
return { combo, type: isUp ? "keyup" : "keydown", raw: hotkeyStr };
|
|
158
215
|
}
|
|
159
216
|
function isFromInputTarget(e) {
|
|
@@ -283,15 +340,21 @@ function handleEvent(type) {
|
|
|
283
340
|
if (paused) return;
|
|
284
341
|
const fromInput = ignoreEditable && isFromInputTarget(e);
|
|
285
342
|
const comboStr = comboKeyFromEvent(e);
|
|
286
|
-
const
|
|
287
|
-
|
|
343
|
+
const codeStr = codeKeyFromEvent(e);
|
|
344
|
+
const seen = /* @__PURE__ */ new Set();
|
|
288
345
|
const candidates = [];
|
|
289
|
-
for (const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (
|
|
294
|
-
|
|
346
|
+
for (const lookupKey of [comboStr, codeStr]) {
|
|
347
|
+
if (!lookupKey || seen.has(lookupKey)) continue;
|
|
348
|
+
seen.add(lookupKey);
|
|
349
|
+
const bindings = registry.get(lookupKey)?.get(type);
|
|
350
|
+
if (!bindings?.length) continue;
|
|
351
|
+
for (const b of bindings) {
|
|
352
|
+
const o2 = b.options;
|
|
353
|
+
if (fromInput && !o2.allowIn?.(e)) continue;
|
|
354
|
+
if (type === "keydown" && o2.repeat === false && e.repeat) continue;
|
|
355
|
+
if (o2.when && o2.when(e) === false) continue;
|
|
356
|
+
candidates.push(b);
|
|
357
|
+
}
|
|
295
358
|
}
|
|
296
359
|
if (!candidates.length) return;
|
|
297
360
|
const winner = pickWinner(candidates);
|
|
@@ -320,19 +383,37 @@ function destroy() {
|
|
|
320
383
|
plugins.clear();
|
|
321
384
|
bindingIndex.clear();
|
|
322
385
|
}
|
|
386
|
+
const BARE_MOD_TO_KEY = {
|
|
387
|
+
ctrl: "Control",
|
|
388
|
+
shift: "Shift",
|
|
389
|
+
alt: "Alt",
|
|
390
|
+
meta: "Meta"
|
|
391
|
+
};
|
|
323
392
|
function trigger(hotkeyStr, { type: forcedType } = {}) {
|
|
324
393
|
const { combo, type } = parseHotkey(hotkeyStr);
|
|
325
394
|
const comboStr = comboKeyFromParts(combo);
|
|
326
395
|
const actualType = forcedType || type;
|
|
327
396
|
const bindings = registry.get(comboStr)?.get(actualType);
|
|
328
397
|
if (!bindings?.length) return false;
|
|
398
|
+
const eventKey = combo.bareModifier ? BARE_MOD_TO_KEY[combo.bareModifier] : combo.key;
|
|
399
|
+
let altKey = !!combo.alt;
|
|
400
|
+
let ctrlKey = !!combo.ctrl;
|
|
401
|
+
let shiftKey = !!combo.shift;
|
|
402
|
+
let metaKey = !!combo.meta;
|
|
403
|
+
if (combo.bareModifier && actualType === "keyup") {
|
|
404
|
+
if (combo.bareModifier === "alt") altKey = false;
|
|
405
|
+
else if (combo.bareModifier === "ctrl") ctrlKey = false;
|
|
406
|
+
else if (combo.bareModifier === "shift") shiftKey = false;
|
|
407
|
+
else if (combo.bareModifier === "meta") metaKey = false;
|
|
408
|
+
}
|
|
329
409
|
const e = {
|
|
330
410
|
type: actualType,
|
|
331
|
-
key:
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
411
|
+
key: eventKey,
|
|
412
|
+
code: combo.code || null,
|
|
413
|
+
ctrlKey,
|
|
414
|
+
shiftKey,
|
|
415
|
+
altKey,
|
|
416
|
+
metaKey,
|
|
336
417
|
repeat: false,
|
|
337
418
|
preventDefault() {
|
|
338
419
|
},
|
|
@@ -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": ";;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAUA,SAAS,GAAG,QAAQ,MAAM,IAAI,SAAS;AACrC,SAAO,iBAAiB,MAAM,IAAI,OAAO;AACzC,SAAO,MAAM,OAAO,oBAAoB,MAAM,IAAI,OAAO;AAC3D;AAGA,MAAM,QACJ,OAAO,cAAc,eAAe,uBAAuB,KAAK,UAAU,QAAQ;AAGpF,MAAM,iBAAiB,oBAAI,IAAI,CAAC,QAAQ,SAAS,OAAO,MAAM,CAAC;
|
|
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// --- 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// --- 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 const off = () => removeById(binding.id)\n off.id = binding.id\n off.hotkey = raw\n return off\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}\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 // 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
|
}
|
package/dist/hotkey-router.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// hotkey-router v0.0
|
|
1
|
+
// hotkey-router v0.1.0 | SEE LICENSE IN LICENSE
|
|
2
2
|
|
|
3
3
|
function on(target, type, fn, options) {
|
|
4
4
|
target.addEventListener(type, fn, options);
|
|
@@ -6,6 +6,14 @@ function on(target, type, fn, options) {
|
|
|
6
6
|
}
|
|
7
7
|
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
8
8
|
const modifierTokens = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "meta"]);
|
|
9
|
+
const MODIFIER_KEY_TOKEN = Object.freeze({
|
|
10
|
+
Alt: "alt",
|
|
11
|
+
AltGraph: "alt",
|
|
12
|
+
Control: "ctrl",
|
|
13
|
+
Shift: "shift",
|
|
14
|
+
Meta: "meta",
|
|
15
|
+
OS: "meta"
|
|
16
|
+
});
|
|
9
17
|
const ahkPrefixMap = /* @__PURE__ */ new Map([
|
|
10
18
|
["^", "ctrl"],
|
|
11
19
|
// Ctrl
|
|
@@ -72,15 +80,19 @@ function normalizeKey(key) {
|
|
|
72
80
|
const k = raw.toLowerCase();
|
|
73
81
|
return keyAliases[k] ?? (raw === " " ? " " : k);
|
|
74
82
|
}
|
|
75
|
-
function comboKeyFromParts({ ctrl, shift, alt, meta, key }) {
|
|
83
|
+
function comboKeyFromParts({ ctrl, shift, alt, meta, key, code, bareModifier }) {
|
|
84
|
+
if (bareModifier) return `__bare__:${bareModifier}`;
|
|
76
85
|
let s = "";
|
|
77
86
|
if (ctrl) s += "ctrl+";
|
|
78
87
|
if (shift) s += "shift+";
|
|
79
88
|
if (alt) s += "alt+";
|
|
80
89
|
if (meta) s += "meta+";
|
|
90
|
+
if (code) return s + "code:" + code;
|
|
81
91
|
return s ? s + key : key;
|
|
82
92
|
}
|
|
83
93
|
function comboKeyFromEvent(e) {
|
|
94
|
+
const bareMod = MODIFIER_KEY_TOKEN[e.key];
|
|
95
|
+
if (bareMod) return `__bare__:${bareMod}`;
|
|
84
96
|
return comboKeyFromParts({
|
|
85
97
|
ctrl: e.ctrlKey,
|
|
86
98
|
shift: e.shiftKey,
|
|
@@ -89,22 +101,52 @@ function comboKeyFromEvent(e) {
|
|
|
89
101
|
key: normalizeKey(e.key)
|
|
90
102
|
});
|
|
91
103
|
}
|
|
104
|
+
function codeKeyFromEvent(e) {
|
|
105
|
+
if (!e.code) return null;
|
|
106
|
+
let s = "";
|
|
107
|
+
if (e.ctrlKey) s += "ctrl+";
|
|
108
|
+
if (e.shiftKey) s += "shift+";
|
|
109
|
+
if (e.altKey) s += "alt+";
|
|
110
|
+
if (e.metaKey) s += "meta+";
|
|
111
|
+
return s + "code:" + e.code;
|
|
112
|
+
}
|
|
92
113
|
function isModifierToken(token) {
|
|
93
114
|
const t = normalizeKey(token);
|
|
94
115
|
return modifierTokens.has(t);
|
|
95
116
|
}
|
|
117
|
+
const CODE_TOKEN_RE = /(^|\+)code:([A-Za-z][A-Za-z0-9_]*)/;
|
|
96
118
|
function parseHotkey(hotkeyStr) {
|
|
97
119
|
const raw = String(hotkeyStr || "").trim();
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
120
|
+
const isUp = /\s+up$/i.test(raw);
|
|
121
|
+
const baseRaw = isUp ? raw.replace(/\s+up$/i, "").trim() : raw;
|
|
122
|
+
const combo = {
|
|
123
|
+
ctrl: false,
|
|
124
|
+
shift: false,
|
|
125
|
+
alt: false,
|
|
126
|
+
meta: false,
|
|
127
|
+
key: null,
|
|
128
|
+
code: null,
|
|
129
|
+
bareModifier: null
|
|
130
|
+
};
|
|
131
|
+
let stripped = baseRaw;
|
|
132
|
+
while (stripped.length) {
|
|
133
|
+
const mod = ahkPrefixMap.get(stripped[0]) ?? null;
|
|
104
134
|
if (!mod) break;
|
|
105
135
|
combo[mod] = true;
|
|
106
|
-
|
|
136
|
+
stripped = stripped.slice(1).trimStart();
|
|
137
|
+
}
|
|
138
|
+
const codeMatches = stripped.match(/code:[A-Za-z][A-Za-z0-9_]*/g);
|
|
139
|
+
if (codeMatches && codeMatches.length > 1) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Invalid hotkey "${hotkeyStr}": multiple code: tokens not supported (got ${codeMatches.length}).`
|
|
142
|
+
);
|
|
107
143
|
}
|
|
144
|
+
let codeValue = null;
|
|
145
|
+
const scrubbed = stripped.replace(CODE_TOKEN_RE, (_, sep, val) => {
|
|
146
|
+
codeValue = val;
|
|
147
|
+
return sep + "__code__";
|
|
148
|
+
});
|
|
149
|
+
let cleanStr = scrubbed.toLowerCase();
|
|
108
150
|
const rawParts = cleanStr.split("+").map((p) => p.trim());
|
|
109
151
|
const parts = rawParts.filter(Boolean);
|
|
110
152
|
const impliedPlusKey = rawParts.some((p) => p === "") && cleanStr.includes("++");
|
|
@@ -123,6 +165,10 @@ function parseHotkey(hotkeyStr) {
|
|
|
123
165
|
}
|
|
124
166
|
}
|
|
125
167
|
for (const part of parts) {
|
|
168
|
+
if (part === "__code__") {
|
|
169
|
+
combo.code = codeValue;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
126
172
|
const actual = keyAliases[part] ?? part;
|
|
127
173
|
if (actual === "ctrl") combo.ctrl = true;
|
|
128
174
|
else if (actual === "shift") combo.shift = true;
|
|
@@ -130,8 +176,19 @@ function parseHotkey(hotkeyStr) {
|
|
|
130
176
|
else if (actual === "meta") combo.meta = true;
|
|
131
177
|
else combo.key = normalizeKey(actual);
|
|
132
178
|
}
|
|
133
|
-
if (!combo.key && impliedPlusKey) combo.key = "+";
|
|
134
|
-
if (!combo.key
|
|
179
|
+
if (!combo.key && !combo.code && impliedPlusKey) combo.key = "+";
|
|
180
|
+
if (!combo.key && !combo.code) {
|
|
181
|
+
const modCount = (combo.ctrl ? 1 : 0) + (combo.shift ? 1 : 0) + (combo.alt ? 1 : 0) + (combo.meta ? 1 : 0);
|
|
182
|
+
if (modCount === 1) {
|
|
183
|
+
combo.bareModifier = combo.ctrl ? "ctrl" : combo.shift ? "shift" : combo.alt ? "alt" : "meta";
|
|
184
|
+
} else if (modCount > 1) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`Invalid hotkey "${hotkeyStr}": multi-modifier bare bindings not supported. Add a base key (e.g. "ctrl+alt+x").`
|
|
187
|
+
);
|
|
188
|
+
} else {
|
|
189
|
+
throw new Error(`Invalid hotkey "${hotkeyStr}" (missing base key)`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
135
192
|
return { combo, type: isUp ? "keyup" : "keydown", raw: hotkeyStr };
|
|
136
193
|
}
|
|
137
194
|
function isFromInputTarget(e) {
|
|
@@ -261,15 +318,21 @@ function handleEvent(type) {
|
|
|
261
318
|
if (paused) return;
|
|
262
319
|
const fromInput = ignoreEditable && isFromInputTarget(e);
|
|
263
320
|
const comboStr = comboKeyFromEvent(e);
|
|
264
|
-
const
|
|
265
|
-
|
|
321
|
+
const codeStr = codeKeyFromEvent(e);
|
|
322
|
+
const seen = /* @__PURE__ */ new Set();
|
|
266
323
|
const candidates = [];
|
|
267
|
-
for (const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
|
|
324
|
+
for (const lookupKey of [comboStr, codeStr]) {
|
|
325
|
+
if (!lookupKey || seen.has(lookupKey)) continue;
|
|
326
|
+
seen.add(lookupKey);
|
|
327
|
+
const bindings = registry.get(lookupKey)?.get(type);
|
|
328
|
+
if (!bindings?.length) continue;
|
|
329
|
+
for (const b of bindings) {
|
|
330
|
+
const o2 = b.options;
|
|
331
|
+
if (fromInput && !o2.allowIn?.(e)) continue;
|
|
332
|
+
if (type === "keydown" && o2.repeat === false && e.repeat) continue;
|
|
333
|
+
if (o2.when && o2.when(e) === false) continue;
|
|
334
|
+
candidates.push(b);
|
|
335
|
+
}
|
|
273
336
|
}
|
|
274
337
|
if (!candidates.length) return;
|
|
275
338
|
const winner = pickWinner(candidates);
|
|
@@ -298,19 +361,37 @@ function destroy() {
|
|
|
298
361
|
plugins.clear();
|
|
299
362
|
bindingIndex.clear();
|
|
300
363
|
}
|
|
364
|
+
const BARE_MOD_TO_KEY = {
|
|
365
|
+
ctrl: "Control",
|
|
366
|
+
shift: "Shift",
|
|
367
|
+
alt: "Alt",
|
|
368
|
+
meta: "Meta"
|
|
369
|
+
};
|
|
301
370
|
function trigger(hotkeyStr, { type: forcedType } = {}) {
|
|
302
371
|
const { combo, type } = parseHotkey(hotkeyStr);
|
|
303
372
|
const comboStr = comboKeyFromParts(combo);
|
|
304
373
|
const actualType = forcedType || type;
|
|
305
374
|
const bindings = registry.get(comboStr)?.get(actualType);
|
|
306
375
|
if (!bindings?.length) return false;
|
|
376
|
+
const eventKey = combo.bareModifier ? BARE_MOD_TO_KEY[combo.bareModifier] : combo.key;
|
|
377
|
+
let altKey = !!combo.alt;
|
|
378
|
+
let ctrlKey = !!combo.ctrl;
|
|
379
|
+
let shiftKey = !!combo.shift;
|
|
380
|
+
let metaKey = !!combo.meta;
|
|
381
|
+
if (combo.bareModifier && actualType === "keyup") {
|
|
382
|
+
if (combo.bareModifier === "alt") altKey = false;
|
|
383
|
+
else if (combo.bareModifier === "ctrl") ctrlKey = false;
|
|
384
|
+
else if (combo.bareModifier === "shift") shiftKey = false;
|
|
385
|
+
else if (combo.bareModifier === "meta") metaKey = false;
|
|
386
|
+
}
|
|
307
387
|
const e = {
|
|
308
388
|
type: actualType,
|
|
309
|
-
key:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
389
|
+
key: eventKey,
|
|
390
|
+
code: combo.code || null,
|
|
391
|
+
ctrlKey,
|
|
392
|
+
shiftKey,
|
|
393
|
+
altKey,
|
|
394
|
+
metaKey,
|
|
314
395
|
repeat: false,
|
|
315
396
|
preventDefault() {
|
|
316
397
|
},
|