hotkey-router 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +285 -0
- package/README.md +315 -0
- package/dist/hotkey-router.cjs +371 -0
- package/dist/hotkey-router.cjs.map +7 -0
- package/dist/hotkey-router.js +352 -0
- package/dist/hotkey-router.js.map +7 -0
- package/dist/hotkey-router.min.js +3 -0
- package/package.json +50 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// hotkey-router v0.0.1 | SEE LICENSE IN LICENSE
|
|
2
|
+
|
|
3
|
+
function on(target, type, fn, options) {
|
|
4
|
+
target.addEventListener(type, fn, options);
|
|
5
|
+
return () => target.removeEventListener(type, fn, options);
|
|
6
|
+
}
|
|
7
|
+
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
8
|
+
const modifierTokens = /* @__PURE__ */ new Set(["ctrl", "shift", "alt", "meta"]);
|
|
9
|
+
const ahkPrefixMap = /* @__PURE__ */ new Map([
|
|
10
|
+
["^", "ctrl"],
|
|
11
|
+
// Ctrl
|
|
12
|
+
["!", "alt"],
|
|
13
|
+
// Alt
|
|
14
|
+
["+", "shift"],
|
|
15
|
+
// Shift
|
|
16
|
+
["#", "meta"]
|
|
17
|
+
// Win / Command
|
|
18
|
+
]);
|
|
19
|
+
const keyAliases = Object.freeze({
|
|
20
|
+
// common
|
|
21
|
+
esc: "escape",
|
|
22
|
+
escape: "escape",
|
|
23
|
+
space: " ",
|
|
24
|
+
spacebar: " ",
|
|
25
|
+
enter: "enter",
|
|
26
|
+
return: "enter",
|
|
27
|
+
del: "delete",
|
|
28
|
+
delete: "delete",
|
|
29
|
+
backspace: "backspace",
|
|
30
|
+
tab: "tab",
|
|
31
|
+
// arrows
|
|
32
|
+
up: "arrowup",
|
|
33
|
+
down: "arrowdown",
|
|
34
|
+
left: "arrowleft",
|
|
35
|
+
right: "arrowright",
|
|
36
|
+
// navigation
|
|
37
|
+
home: "home",
|
|
38
|
+
end: "end",
|
|
39
|
+
pageup: "pageup",
|
|
40
|
+
pagedown: "pagedown",
|
|
41
|
+
pgup: "pageup",
|
|
42
|
+
pgdn: "pagedown",
|
|
43
|
+
// modifiers (words)
|
|
44
|
+
cmd: "meta",
|
|
45
|
+
command: "meta",
|
|
46
|
+
win: "meta",
|
|
47
|
+
meta: "meta",
|
|
48
|
+
control: "ctrl",
|
|
49
|
+
ctrl: "ctrl",
|
|
50
|
+
option: "alt",
|
|
51
|
+
alt: "alt",
|
|
52
|
+
shift: "shift",
|
|
53
|
+
// modifiers (symbols)
|
|
54
|
+
"\u2318": "meta",
|
|
55
|
+
"\u2303": "ctrl",
|
|
56
|
+
"\u2325": "alt",
|
|
57
|
+
"\u21E7": "shift",
|
|
58
|
+
// cross-platform "mod"
|
|
59
|
+
mod: isMac ? "meta" : "ctrl",
|
|
60
|
+
// plus key ergonomics
|
|
61
|
+
plus: "+"
|
|
62
|
+
});
|
|
63
|
+
let ignoreEditable = true;
|
|
64
|
+
let defaultTarget = typeof window !== "undefined" ? window : null;
|
|
65
|
+
const registry = /* @__PURE__ */ new Map();
|
|
66
|
+
const plugins = /* @__PURE__ */ new Map();
|
|
67
|
+
const bindingIndex = /* @__PURE__ */ new Map();
|
|
68
|
+
let paused = false;
|
|
69
|
+
let nextId = 1;
|
|
70
|
+
function normalizeKey(key) {
|
|
71
|
+
const raw = String(key ?? "").trim();
|
|
72
|
+
const k = raw.toLowerCase();
|
|
73
|
+
return keyAliases[k] ?? (raw === " " ? " " : k);
|
|
74
|
+
}
|
|
75
|
+
function comboKeyFromParts({ ctrl, shift, alt, meta, key }) {
|
|
76
|
+
let s = "";
|
|
77
|
+
if (ctrl) s += "ctrl+";
|
|
78
|
+
if (shift) s += "shift+";
|
|
79
|
+
if (alt) s += "alt+";
|
|
80
|
+
if (meta) s += "meta+";
|
|
81
|
+
return s ? s + key : key;
|
|
82
|
+
}
|
|
83
|
+
function comboKeyFromEvent(e) {
|
|
84
|
+
return comboKeyFromParts({
|
|
85
|
+
ctrl: e.ctrlKey,
|
|
86
|
+
shift: e.shiftKey,
|
|
87
|
+
alt: e.altKey,
|
|
88
|
+
meta: e.metaKey,
|
|
89
|
+
key: normalizeKey(e.key)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function isModifierToken(token) {
|
|
93
|
+
const t = normalizeKey(token);
|
|
94
|
+
return modifierTokens.has(t);
|
|
95
|
+
}
|
|
96
|
+
function parseHotkey(hotkeyStr) {
|
|
97
|
+
const raw = String(hotkeyStr || "").trim();
|
|
98
|
+
const lower = raw.toLowerCase();
|
|
99
|
+
const isUp = lower.endsWith(" up");
|
|
100
|
+
let cleanStr = (isUp ? raw.slice(0, -3) : raw).trim().toLowerCase();
|
|
101
|
+
const combo = { ctrl: false, shift: false, alt: false, meta: false, key: null };
|
|
102
|
+
while (cleanStr.length) {
|
|
103
|
+
const mod = ahkPrefixMap.get(cleanStr[0]) ?? null;
|
|
104
|
+
if (!mod) break;
|
|
105
|
+
combo[mod] = true;
|
|
106
|
+
cleanStr = cleanStr.slice(1).trimStart();
|
|
107
|
+
}
|
|
108
|
+
const rawParts = cleanStr.split("+").map((p) => p.trim());
|
|
109
|
+
const parts = rawParts.filter(Boolean);
|
|
110
|
+
const impliedPlusKey = rawParts.some((p) => p === "") && cleanStr.includes("++");
|
|
111
|
+
if (!impliedPlusKey) {
|
|
112
|
+
let seenBase = false;
|
|
113
|
+
for (const part of parts) {
|
|
114
|
+
if (isModifierToken(part)) {
|
|
115
|
+
if (seenBase) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`Invalid hotkey "${hotkeyStr}": modifiers must come before the base key (e.g. "ctrl+k", not "k+ctrl")`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (!seenBase) seenBase = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const part of parts) {
|
|
126
|
+
const actual = keyAliases[part] ?? part;
|
|
127
|
+
if (actual === "ctrl") combo.ctrl = true;
|
|
128
|
+
else if (actual === "shift") combo.shift = true;
|
|
129
|
+
else if (actual === "alt") combo.alt = true;
|
|
130
|
+
else if (actual === "meta") combo.meta = true;
|
|
131
|
+
else combo.key = normalizeKey(actual);
|
|
132
|
+
}
|
|
133
|
+
if (!combo.key && impliedPlusKey) combo.key = "+";
|
|
134
|
+
if (!combo.key) throw new Error(`Invalid hotkey "${hotkeyStr}" (missing base key)`);
|
|
135
|
+
return { combo, type: isUp ? "keyup" : "keydown", raw: hotkeyStr };
|
|
136
|
+
}
|
|
137
|
+
function isFromInputTarget(e) {
|
|
138
|
+
const t = e?.target;
|
|
139
|
+
if (!t) return false;
|
|
140
|
+
const tag = String(t.tagName || "").toUpperCase();
|
|
141
|
+
return t.isContentEditable || tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || t.getAttribute?.("role") === "textbox";
|
|
142
|
+
}
|
|
143
|
+
function normalizeOptions(type, options) {
|
|
144
|
+
const o = options ? { ...options } : {};
|
|
145
|
+
if (type === "keydown" && o.repeat == null) o.repeat = false;
|
|
146
|
+
if (o.priority == null) o.priority = 0;
|
|
147
|
+
return o;
|
|
148
|
+
}
|
|
149
|
+
function ensureSlot(comboStr, type) {
|
|
150
|
+
let typeMap = registry.get(comboStr);
|
|
151
|
+
if (!typeMap) {
|
|
152
|
+
typeMap = /* @__PURE__ */ new Map();
|
|
153
|
+
registry.set(comboStr, typeMap);
|
|
154
|
+
}
|
|
155
|
+
let arr = typeMap.get(type);
|
|
156
|
+
if (!arr) {
|
|
157
|
+
arr = [];
|
|
158
|
+
typeMap.set(type, arr);
|
|
159
|
+
}
|
|
160
|
+
return arr;
|
|
161
|
+
}
|
|
162
|
+
function removeById(id) {
|
|
163
|
+
const idx = bindingIndex.get(id);
|
|
164
|
+
if (!idx) return false;
|
|
165
|
+
const { comboStr, type } = idx;
|
|
166
|
+
const typeMap = registry.get(comboStr);
|
|
167
|
+
const arr = typeMap?.get(type);
|
|
168
|
+
if (!arr?.length) {
|
|
169
|
+
bindingIndex.delete(id);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
const next = arr.filter((b) => b.id !== id);
|
|
173
|
+
if (next.length) typeMap.set(type, next);
|
|
174
|
+
else typeMap.delete(type);
|
|
175
|
+
if (!typeMap.size) registry.delete(comboStr);
|
|
176
|
+
bindingIndex.delete(id);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
function bind(hotkeyStr, handler, plugin = null, options = {}) {
|
|
180
|
+
const { combo, type, raw } = parseHotkey(hotkeyStr);
|
|
181
|
+
const comboStr = comboKeyFromParts(combo);
|
|
182
|
+
const opts = normalizeOptions(type, options);
|
|
183
|
+
const binding = {
|
|
184
|
+
id: nextId++,
|
|
185
|
+
comboStr,
|
|
186
|
+
type,
|
|
187
|
+
combo,
|
|
188
|
+
handler,
|
|
189
|
+
plugin,
|
|
190
|
+
raw,
|
|
191
|
+
options: opts
|
|
192
|
+
};
|
|
193
|
+
ensureSlot(comboStr, type).push(binding);
|
|
194
|
+
bindingIndex.set(binding.id, { comboStr, type });
|
|
195
|
+
const off = () => removeById(binding.id);
|
|
196
|
+
off.id = binding.id;
|
|
197
|
+
off.hotkey = raw;
|
|
198
|
+
return off;
|
|
199
|
+
}
|
|
200
|
+
function unbind(hotkeyStr, handler) {
|
|
201
|
+
const { combo, type } = parseHotkey(hotkeyStr);
|
|
202
|
+
const comboStr = comboKeyFromParts(combo);
|
|
203
|
+
const typeMap = registry.get(comboStr);
|
|
204
|
+
if (!typeMap) return;
|
|
205
|
+
const arr = typeMap.get(type);
|
|
206
|
+
if (!arr?.length) return;
|
|
207
|
+
if (!handler) {
|
|
208
|
+
for (const b of arr) bindingIndex.delete(b.id);
|
|
209
|
+
typeMap.delete(type);
|
|
210
|
+
} else {
|
|
211
|
+
const keep = [];
|
|
212
|
+
for (const b of arr) {
|
|
213
|
+
if (b.handler === handler) bindingIndex.delete(b.id);
|
|
214
|
+
else keep.push(b);
|
|
215
|
+
}
|
|
216
|
+
if (keep.length) typeMap.set(type, keep);
|
|
217
|
+
else typeMap.delete(type);
|
|
218
|
+
}
|
|
219
|
+
if (!typeMap.size) registry.delete(comboStr);
|
|
220
|
+
}
|
|
221
|
+
function registerPlugin(name, hotkeyMap) {
|
|
222
|
+
if (plugins.has(name)) throw new Error(`Plugin "${name}" already registered`);
|
|
223
|
+
const ids = [];
|
|
224
|
+
for (const [hotkeyStr, fnOrTuple] of Object.entries(hotkeyMap)) {
|
|
225
|
+
const [fn, options] = Array.isArray(fnOrTuple) ? fnOrTuple : [fnOrTuple, {}];
|
|
226
|
+
const off = bind(hotkeyStr, fn, name, options);
|
|
227
|
+
ids.push(off.id);
|
|
228
|
+
}
|
|
229
|
+
plugins.set(name, ids);
|
|
230
|
+
return () => unregisterPlugin(name);
|
|
231
|
+
}
|
|
232
|
+
function unregisterPlugin(name) {
|
|
233
|
+
const ids = plugins.get(name);
|
|
234
|
+
if (!ids) return;
|
|
235
|
+
for (const id of ids) removeById(id);
|
|
236
|
+
plugins.delete(name);
|
|
237
|
+
}
|
|
238
|
+
function pause() {
|
|
239
|
+
paused = true;
|
|
240
|
+
}
|
|
241
|
+
function resume() {
|
|
242
|
+
paused = false;
|
|
243
|
+
}
|
|
244
|
+
function ignoreInput(value = true) {
|
|
245
|
+
ignoreEditable = !!value;
|
|
246
|
+
}
|
|
247
|
+
function setTarget(target) {
|
|
248
|
+
defaultTarget = target;
|
|
249
|
+
}
|
|
250
|
+
function pickWinner(bindings) {
|
|
251
|
+
let best = null;
|
|
252
|
+
for (const b of bindings) {
|
|
253
|
+
if (!best) best = b;
|
|
254
|
+
else if (b.options.priority > best.options.priority) best = b;
|
|
255
|
+
else if (b.options.priority === best.options.priority && b.id > best.id) best = b;
|
|
256
|
+
}
|
|
257
|
+
return best;
|
|
258
|
+
}
|
|
259
|
+
function handleEvent(type) {
|
|
260
|
+
return (e) => {
|
|
261
|
+
if (paused) return;
|
|
262
|
+
const fromInput = ignoreEditable && isFromInputTarget(e);
|
|
263
|
+
const comboStr = comboKeyFromEvent(e);
|
|
264
|
+
const bindings = registry.get(comboStr)?.get(type);
|
|
265
|
+
if (!bindings?.length) return;
|
|
266
|
+
const candidates = [];
|
|
267
|
+
for (const b of bindings) {
|
|
268
|
+
const o2 = b.options;
|
|
269
|
+
if (fromInput && !o2.allowIn?.(e)) continue;
|
|
270
|
+
if (type === "keydown" && o2.repeat === false && e.repeat) continue;
|
|
271
|
+
if (o2.when && o2.when(e) === false) continue;
|
|
272
|
+
candidates.push(b);
|
|
273
|
+
}
|
|
274
|
+
if (!candidates.length) return;
|
|
275
|
+
const winner = pickWinner(candidates);
|
|
276
|
+
const o = winner.options;
|
|
277
|
+
if (o.preventDefault) e.preventDefault();
|
|
278
|
+
if (o.stopImmediatePropagation) e.stopImmediatePropagation();
|
|
279
|
+
else if (o.stopPropagation) e.stopPropagation();
|
|
280
|
+
winner.handler(e);
|
|
281
|
+
if (o.once) removeById(winner.id);
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
let stopDown = null;
|
|
285
|
+
let stopUp = null;
|
|
286
|
+
function init({ target = defaultTarget, capture = false } = {}) {
|
|
287
|
+
if (!target) throw new Error("hotkeys.init() requires a target (e.g. window)");
|
|
288
|
+
if (stopDown || stopUp) destroy();
|
|
289
|
+
stopDown = on(target, "keydown", handleEvent("keydown"), { capture });
|
|
290
|
+
stopUp = on(target, "keyup", handleEvent("keyup"), { capture });
|
|
291
|
+
}
|
|
292
|
+
function destroy() {
|
|
293
|
+
stopDown?.();
|
|
294
|
+
stopUp?.();
|
|
295
|
+
stopDown = null;
|
|
296
|
+
stopUp = null;
|
|
297
|
+
registry.clear();
|
|
298
|
+
plugins.clear();
|
|
299
|
+
bindingIndex.clear();
|
|
300
|
+
}
|
|
301
|
+
function trigger(hotkeyStr, { type: forcedType } = {}) {
|
|
302
|
+
const { combo, type } = parseHotkey(hotkeyStr);
|
|
303
|
+
const comboStr = comboKeyFromParts(combo);
|
|
304
|
+
const actualType = forcedType || type;
|
|
305
|
+
const bindings = registry.get(comboStr)?.get(actualType);
|
|
306
|
+
if (!bindings?.length) return false;
|
|
307
|
+
const e = {
|
|
308
|
+
type: actualType,
|
|
309
|
+
key: combo.key,
|
|
310
|
+
ctrlKey: !!combo.ctrl,
|
|
311
|
+
shiftKey: !!combo.shift,
|
|
312
|
+
altKey: !!combo.alt,
|
|
313
|
+
metaKey: !!combo.meta,
|
|
314
|
+
repeat: false,
|
|
315
|
+
preventDefault() {
|
|
316
|
+
},
|
|
317
|
+
stopPropagation() {
|
|
318
|
+
},
|
|
319
|
+
stopImmediatePropagation() {
|
|
320
|
+
},
|
|
321
|
+
target: null
|
|
322
|
+
};
|
|
323
|
+
const winner = pickWinner(
|
|
324
|
+
bindings.filter((b) => b.options.when ? b.options.when(e) !== false : true)
|
|
325
|
+
);
|
|
326
|
+
if (!winner) return false;
|
|
327
|
+
winner.handler(e);
|
|
328
|
+
if (winner.options.once) removeById(winner.id);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
if (defaultTarget) init();
|
|
332
|
+
var hotkey_router_default = {
|
|
333
|
+
// lifecycle
|
|
334
|
+
init,
|
|
335
|
+
destroy,
|
|
336
|
+
setTarget,
|
|
337
|
+
// bindings
|
|
338
|
+
bind,
|
|
339
|
+
unbind,
|
|
340
|
+
registerPlugin,
|
|
341
|
+
unregisterPlugin,
|
|
342
|
+
// control
|
|
343
|
+
pause,
|
|
344
|
+
resume,
|
|
345
|
+
ignoreInput,
|
|
346
|
+
// testing / automation
|
|
347
|
+
trigger
|
|
348
|
+
};
|
|
349
|
+
export {
|
|
350
|
+
hotkey_router_default as default
|
|
351
|
+
};
|
|
352
|
+
//# sourceMappingURL=hotkey-router.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../hotkey-router.js"],
|
|
4
|
+
"sourcesContent": ["// hotkey-router.js\r\n// Hotkey Router \u2014 plugin-first keyboard shortcut router (tiny + predictable)\r\n//\r\n// Goals:\r\n// - O(1) dispatch by combo string lookup\r\n// - Deterministic routing: highest priority wins; ties -> most recently bound wins\r\n// - Safe plugin cleanup (never deletes other plugins\u2019 bindings)\r\n// - Modern browser target (KeyboardEvent.key + addEventListener)\r\n\r\n// --- tiny event helper ---\r\nfunction on(target, type, fn, options) {\r\n target.addEventListener(type, fn, options)\r\n return () => target.removeEventListener(type, fn, options)\r\n}\r\n\r\n// --- key alias map ---\r\nconst isMac =\r\n typeof navigator !== 'undefined' && /Mac|iPhone|iPad|iPod/.test(navigator.platform)\r\n\r\n\r\nconst modifierTokens = new Set(['ctrl', 'shift', 'alt', 'meta'])\r\n\r\nconst ahkPrefixMap = new Map([\r\n ['^', 'ctrl'], // Ctrl\r\n ['!', 'alt'], // Alt\r\n ['+', 'shift'], // Shift\r\n ['#', 'meta'], // Win / Command\r\n])\r\n\r\n\r\nconst keyAliases = Object.freeze({\r\n // common\r\n esc: 'escape',\r\n escape: 'escape',\r\n space: ' ',\r\n spacebar: ' ',\r\n enter: 'enter',\r\n return: 'enter',\r\n del: 'delete',\r\n delete: 'delete',\r\n backspace: 'backspace',\r\n tab: 'tab',\r\n\r\n // arrows\r\n up: 'arrowup',\r\n down: 'arrowdown',\r\n left: 'arrowleft',\r\n right: 'arrowright',\r\n\r\n // navigation\r\n home: 'home',\r\n end: 'end',\r\n pageup: 'pageup',\r\n pagedown: 'pagedown',\r\n pgup: 'pageup',\r\n pgdn: 'pagedown',\r\n\r\n // modifiers (words)\r\n cmd: 'meta',\r\n command: 'meta',\r\n win: 'meta',\r\n meta: 'meta',\r\n control: 'ctrl',\r\n ctrl: 'ctrl',\r\n option: 'alt',\r\n alt: 'alt',\r\n shift: 'shift',\r\n\r\n // modifiers (symbols)\r\n '\u2318': 'meta',\r\n '\u2303': 'ctrl',\r\n '\u2325': 'alt',\r\n '\u21E7': 'shift',\r\n\r\n // cross-platform \"mod\"\r\n mod: isMac ? 'meta' : 'ctrl',\r\n\r\n // plus key ergonomics\r\n plus: '+',\r\n})\r\n\r\n// --- config ---\r\nlet ignoreEditable = true\r\nlet defaultTarget = typeof window !== 'undefined' ? window : null\r\n\r\n// --- state ---\r\n// comboStr -> Map<type, Array<Binding>>\r\nconst registry = new Map()\r\n// pluginName -> Array<bindingId>\r\nconst plugins = new Map()\r\n// bindingId -> { comboStr, type }\r\nconst bindingIndex = new Map()\r\n\r\nlet paused = false\r\nlet nextId = 1\r\n\r\n// --- helpers ---\r\nfunction normalizeKey(key) {\r\n const raw = String(key ?? '').trim()\r\n const k = raw.toLowerCase()\r\n return keyAliases[k] ?? (raw === ' ' ? ' ' : k)\r\n}\r\n\r\nfunction comboKeyFromParts({ ctrl, shift, alt, meta, key }) {\r\n let s = ''\r\n if (ctrl) s += 'ctrl+'\r\n if (shift) s += 'shift+'\r\n if (alt) s += 'alt+'\r\n if (meta) s += 'meta+'\r\n return s ? s + key : key\r\n}\r\n\r\nfunction comboKeyFromEvent(e) {\r\n return comboKeyFromParts({\r\n ctrl: e.ctrlKey,\r\n shift: e.shiftKey,\r\n alt: e.altKey,\r\n meta: e.metaKey,\r\n key: normalizeKey(e.key),\r\n })\r\n}\r\n\r\n\r\nfunction isModifierToken(token) {\r\n const t = normalizeKey(token)\r\n return modifierTokens.has(t)\r\n}\r\n\r\n\r\nfunction parseHotkey(hotkeyStr) {\r\n const raw = String(hotkeyStr || '').trim()\r\n const lower = raw.toLowerCase()\r\n\r\n const isUp = lower.endsWith(' up')\r\n let cleanStr = (isUp ? raw.slice(0, -3) : raw).trim().toLowerCase()\r\n\r\n // --- AHK-style prefix modifiers ---\r\n // Examples:\r\n // ^k => ctrl+k\r\n // !k => alt+k\r\n // +k => shift+k\r\n // #k => meta+k\r\n // ^!k => ctrl+alt+k\r\n //\r\n // This runs BEFORE normal \"+\" token parsing so it composes cleanly.\r\n const combo = { ctrl: false, shift: false, alt: false, meta: false, key: null }\r\n\r\n while (cleanStr.length) {\r\n const mod = ahkPrefixMap.get(cleanStr[0]) ?? null\r\n if (!mod) break\r\n combo[mod] = true\r\n cleanStr = cleanStr.slice(1).trimStart()\r\n }\r\n\r\n // Allow plus key:\r\n // - \"ctrl++\" => ctrl + \"+\"\r\n // - \"++\" => shift + \"+\" (AHK '+' prefix sets shift, remaining '+' becomes the base key)\r\n // `split('+')` produces empties; we keep that signal.\r\n const rawParts = cleanStr.split('+').map((p) => p.trim())\r\n const parts = rawParts.filter(Boolean)\r\n const impliedPlusKey = rawParts.some((p) => p === '') && cleanStr.includes('++')\r\n\r\n // Enforce: modifiers must come before the base key.\r\n // Example: \"ctrl+k\" \u2705 | \"k+ctrl\" \u274C\r\n // We ignore the special \"+ key\" case (ctrl++) since it has no explicit base token.\r\n if (!impliedPlusKey) {\r\n let seenBase = false\r\n for (const part of parts) {\r\n if (isModifierToken(part)) {\r\n if (seenBase) {\r\n throw new Error(\r\n `Invalid hotkey \"${hotkeyStr}\": modifiers must come before the base key (e.g. \"ctrl+k\", not \"k+ctrl\")`\r\n )\r\n }\r\n } else {\r\n if (!seenBase) seenBase = true\r\n }\r\n }\r\n }\r\n\r\n // Continue parsing the remaining tokens (words/symbols/aliases)\r\n for (const part of parts) {\r\n const actual = keyAliases[part] ?? part\r\n if (actual === 'ctrl') combo.ctrl = true\r\n else if (actual === 'shift') combo.shift = true\r\n else if (actual === 'alt') combo.alt = true\r\n else if (actual === 'meta') combo.meta = true\r\n else combo.key = normalizeKey(actual)\r\n }\r\n\r\n if (!combo.key && impliedPlusKey) combo.key = '+'\r\n if (!combo.key) throw new Error(`Invalid hotkey \"${hotkeyStr}\" (missing base key)`)\r\n\r\n return { combo, type: isUp ? 'keyup' : 'keydown', raw: hotkeyStr }\r\n}\r\n\r\n\r\nfunction isFromInputTarget(e) {\r\n const t = e?.target\r\n if (!t) return false\r\n const tag = String(t.tagName || '').toUpperCase()\r\n return (\r\n t.isContentEditable ||\r\n tag === 'INPUT' ||\r\n tag === 'TEXTAREA' ||\r\n tag === 'SELECT' ||\r\n t.getAttribute?.('role') === 'textbox'\r\n )\r\n}\r\n\r\n\r\n// --- binding model ---\r\n// options:\r\n// preventDefault, stopPropagation, stopImmediatePropagation\r\n// repeat (default false on keydown), once\r\n// when(e) -> boolean (gate)\r\n// allowIn(e) -> boolean (override ignoreInput)\r\n// priority (higher wins)\r\nfunction normalizeOptions(type, options) {\r\n const o = options ? { ...options } : {}\r\n if (type === 'keydown' && o.repeat == null) o.repeat = false\r\n if (o.priority == null) o.priority = 0\r\n return o\r\n}\r\n\r\n\r\nfunction ensureSlot(comboStr, type) {\r\n let typeMap = registry.get(comboStr)\r\n if (!typeMap) {\r\n typeMap = new Map()\r\n registry.set(comboStr, typeMap)\r\n }\r\n let arr = typeMap.get(type)\r\n if (!arr) {\r\n arr = []\r\n typeMap.set(type, arr)\r\n }\r\n return arr\r\n}\r\n\r\n\r\nfunction removeById(id) {\r\n const idx = bindingIndex.get(id)\r\n if (!idx) return false\r\n\r\n const { comboStr, type } = idx\r\n const typeMap = registry.get(comboStr)\r\n const arr = typeMap?.get(type)\r\n if (!arr?.length) {\r\n bindingIndex.delete(id)\r\n return false\r\n }\r\n\r\n const next = arr.filter((b) => b.id !== id)\r\n if (next.length) typeMap.set(type, next)\r\n else typeMap.delete(type)\r\n\r\n if (!typeMap.size) registry.delete(comboStr)\r\n bindingIndex.delete(id)\r\n return true\r\n}\r\n\r\n\r\n// --- public API: bind / unbind ---\r\n// bind returns an off() function; off.id is exposed for plugin bookkeeping.\r\nfunction bind(hotkeyStr, handler, plugin = null, options = {}) {\r\n const { combo, type, raw } = parseHotkey(hotkeyStr)\r\n const comboStr = comboKeyFromParts(combo)\r\n const opts = normalizeOptions(type, options)\r\n\r\n const binding = {\r\n id: nextId++,\r\n comboStr,\r\n type,\r\n combo,\r\n handler,\r\n plugin,\r\n raw,\r\n options: opts,\r\n }\r\n\r\n ensureSlot(comboStr, type).push(binding)\r\n bindingIndex.set(binding.id, { comboStr, type })\r\n\r\n const off = () => removeById(binding.id)\r\n off.id = binding.id\r\n off.hotkey = raw\r\n return off\r\n}\r\n\r\n\r\n// unbind('ctrl+k') -> remove all for that combo/type\r\n// unbind('ctrl+k', fn) -> remove only that handler\r\nfunction unbind(hotkeyStr, handler) {\r\n const { combo, type } = parseHotkey(hotkeyStr)\r\n const comboStr = comboKeyFromParts(combo)\r\n const typeMap = registry.get(comboStr)\r\n if (!typeMap) return\r\n\r\n const arr = typeMap.get(type)\r\n if (!arr?.length) return\r\n\r\n if (!handler) {\r\n for (const b of arr) bindingIndex.delete(b.id)\r\n typeMap.delete(type)\r\n } else {\r\n const keep = []\r\n for (const b of arr) {\r\n if (b.handler === handler) bindingIndex.delete(b.id)\r\n else keep.push(b)\r\n }\r\n if (keep.length) typeMap.set(type, keep)\r\n else typeMap.delete(type)\r\n }\r\n\r\n if (!typeMap.size) registry.delete(comboStr)\r\n}\r\n\r\n\r\n// --- plugin system ---\r\n// registerPlugin returns unregister function.\r\nfunction registerPlugin(name, hotkeyMap) {\r\n if (plugins.has(name)) throw new Error(`Plugin \"${name}\" already registered`)\r\n const ids = []\r\n\r\n for (const [hotkeyStr, fnOrTuple] of Object.entries(hotkeyMap)) {\r\n const [fn, options] = Array.isArray(fnOrTuple) ? fnOrTuple : [fnOrTuple, {}]\r\n const off = bind(hotkeyStr, fn, name, options)\r\n ids.push(off.id)\r\n }\r\n\r\n plugins.set(name, ids)\r\n return () => unregisterPlugin(name)\r\n}\r\n\r\nfunction unregisterPlugin(name) {\r\n const ids = plugins.get(name)\r\n if (!ids) return\r\n for (const id of ids) removeById(id)\r\n plugins.delete(name)\r\n}\r\n\r\n// --- control ---\r\nfunction pause() {\r\n paused = true\r\n}\r\n\r\nfunction resume() {\r\n paused = false\r\n}\r\n\r\nfunction ignoreInput(value = true) {\r\n ignoreEditable = !!value\r\n}\r\n\r\n// allow changing the default target (iframes/tests).\r\n// we intentionally do not auto-rebind to keep the core tiny.\r\nfunction setTarget(target) {\r\n defaultTarget = target\r\n}\r\n\r\n\r\n// --- dispatch (O(1) lookup) ---\r\nfunction pickWinner(bindings) {\r\n // Highest priority wins; ties -> newest wins (largest id).\r\n let best = null\r\n for (const b of bindings) {\r\n if (!best) best = b\r\n else if (b.options.priority > best.options.priority) best = b\r\n else if (b.options.priority === best.options.priority && b.id > best.id) best = b\r\n }\r\n return best\r\n}\r\n\r\n\r\nfunction handleEvent(type) {\r\n return (e) => {\r\n if (paused) return\r\n\r\n const fromInput = ignoreEditable && isFromInputTarget(e)\r\n const comboStr = comboKeyFromEvent(e)\r\n\r\n const bindings = registry.get(comboStr)?.get(type)\r\n if (!bindings?.length) return\r\n\r\n const candidates = []\r\n for (const b of bindings) {\r\n const o = b.options\r\n if (fromInput && !o.allowIn?.(e)) continue\r\n if (type === 'keydown' && o.repeat === false && e.repeat) continue\r\n if (o.when && o.when(e) === false) continue\r\n candidates.push(b)\r\n }\r\n if (!candidates.length) return\r\n\r\n const winner = pickWinner(candidates)\r\n const o = winner.options\r\n\r\n if (o.preventDefault) e.preventDefault()\r\n if (o.stopImmediatePropagation) e.stopImmediatePropagation()\r\n else if (o.stopPropagation) e.stopPropagation()\r\n\r\n winner.handler(e)\r\n\r\n if (o.once) removeById(winner.id)\r\n }\r\n}\r\n\r\n\r\n// --- listeners ---\r\nlet stopDown = null\r\nlet stopUp = null\r\n\r\nfunction init({ target = defaultTarget, capture = false } = {}) {\r\n if (!target) throw new Error('hotkeys.init() requires a target (e.g. window)')\r\n if (stopDown || stopUp) destroy()\r\n\r\n stopDown = on(target, 'keydown', handleEvent('keydown'), { capture })\r\n stopUp = on(target, 'keyup', handleEvent('keyup'), { capture })\r\n}\r\n\r\n\r\nfunction destroy() {\r\n stopDown?.()\r\n stopUp?.()\r\n stopDown = null\r\n stopUp = null\r\n registry.clear()\r\n plugins.clear()\r\n bindingIndex.clear()\r\n}\r\n\r\n\r\n// --- programmatic trigger (tests / automation) ---\r\nfunction trigger(hotkeyStr, { type: forcedType } = {}) {\r\n const { combo, type } = parseHotkey(hotkeyStr)\r\n const comboStr = comboKeyFromParts(combo)\r\n const actualType = forcedType || type\r\n\r\n const bindings = registry.get(comboStr)?.get(actualType)\r\n if (!bindings?.length) return false\r\n\r\n const e = {\r\n type: actualType,\r\n key: combo.key,\r\n ctrlKey: !!combo.ctrl,\r\n shiftKey: !!combo.shift,\r\n altKey: !!combo.alt,\r\n metaKey: !!combo.meta,\r\n repeat: false,\r\n preventDefault() { },\r\n stopPropagation() { },\r\n stopImmediatePropagation() { },\r\n target: null,\r\n }\r\n\r\n const winner = pickWinner(\r\n bindings.filter((b) => (b.options.when ? b.options.when(e) !== false : true))\r\n )\r\n if (!winner) return false\r\n\r\n winner.handler(e)\r\n if (winner.options.once) removeById(winner.id)\r\n return true\r\n}\r\n\r\n\r\n// Auto-init on window by default (browser only)\r\nif (defaultTarget) init()\r\n\r\nexport default {\r\n // lifecycle\r\n init,\r\n destroy,\r\n setTarget,\r\n\r\n // bindings\r\n bind,\r\n unbind,\r\n registerPlugin,\r\n unregisterPlugin,\r\n\r\n // control\r\n pause,\r\n resume,\r\n ignoreInput,\r\n\r\n // testing / automation\r\n trigger,\r\n}"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["o"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// hotkey-router v0.0.1 | SEE LICENSE IN LICENSE
|
|
2
|
+
|
|
3
|
+
function E(t,e,n,r){return t.addEventListener(e,n,r),()=>t.removeEventListener(e,n,r)}const C=typeof navigator<"u"&&/Mac|iPhone|iPad|iPod/.test(navigator.platform),L=new Set(["ctrl","shift","alt","meta"]),z=new Map([["^","ctrl"],["!","alt"],["+","shift"],["#","meta"]]),v=Object.freeze({esc:"escape",escape:"escape",space:" ",spacebar:" ",enter:"enter",return:"enter",del:"delete",delete:"delete",backspace:"backspace",tab:"tab",up:"arrowup",down:"arrowdown",left:"arrowleft",right:"arrowright",home:"home",end:"end",pageup:"pageup",pagedown:"pagedown",pgup:"pageup",pgdn:"pagedown",cmd:"meta",command:"meta",win:"meta",meta:"meta",control:"ctrl",ctrl:"ctrl",option:"alt",alt:"alt",shift:"shift","\u2318":"meta","\u2303":"ctrl","\u2325":"alt","\u21E7":"shift",mod:C?"meta":"ctrl",plus:"+"});let I=!0,h=typeof window<"u"?window:null;const u=new Map,d=new Map,p=new Map;let b=!1,D=1;function k(t){const e=String(t??"").trim(),n=e.toLowerCase();return v[n]??(e===" "?" ":n)}function g({ctrl:t,shift:e,alt:n,meta:r,key:i}){let o="";return t&&(o+="ctrl+"),e&&(o+="shift+"),n&&(o+="alt+"),r&&(o+="meta+"),o?o+i:i}function O(t){return g({ctrl:t.ctrlKey,shift:t.shiftKey,alt:t.altKey,meta:t.metaKey,key:k(t.key)})}function U(t){const e=k(t);return L.has(e)}function P(t){const e=String(t||"").trim(),r=e.toLowerCase().endsWith(" up");let i=(r?e.slice(0,-3):e).trim().toLowerCase();const o={ctrl:!1,shift:!1,alt:!1,meta:!1,key:null};for(;i.length;){const s=z.get(i[0])??null;if(!s)break;o[s]=!0,i=i.slice(1).trimStart()}const l=i.split("+").map(s=>s.trim()),a=l.filter(Boolean),f=l.some(s=>s==="")&&i.includes("++");if(!f){let s=!1;for(const c of a)if(U(c)){if(s)throw new Error(`Invalid hotkey "${t}": modifiers must come before the base key (e.g. "ctrl+k", not "k+ctrl")`)}else s||(s=!0)}for(const s of a){const c=v[s]??s;c==="ctrl"?o.ctrl=!0:c==="shift"?o.shift=!0:c==="alt"?o.alt=!0:c==="meta"?o.meta=!0:o.key=k(c)}if(!o.key&&f&&(o.key="+"),!o.key)throw new Error(`Invalid hotkey "${t}" (missing base key)`);return{combo:o,type:r?"keyup":"keydown",raw:t}}function B(t){const e=t?.target;if(!e)return!1;const n=String(e.tagName||"").toUpperCase();return e.isContentEditable||n==="INPUT"||n==="TEXTAREA"||n==="SELECT"||e.getAttribute?.("role")==="textbox"}function F(t,e){const n=e?{...e}:{};return t==="keydown"&&n.repeat==null&&(n.repeat=!1),n.priority==null&&(n.priority=0),n}function $(t,e){let n=u.get(t);n||(n=new Map,u.set(t,n));let r=n.get(e);return r||(r=[],n.set(e,r)),r}function m(t){const e=p.get(t);if(!e)return!1;const{comboStr:n,type:r}=e,i=u.get(n),o=i?.get(r);if(!o?.length)return p.delete(t),!1;const l=o.filter(a=>a.id!==t);return l.length?i.set(r,l):i.delete(r),i.size||u.delete(n),p.delete(t),!0}function K(t,e,n=null,r={}){const{combo:i,type:o,raw:l}=P(t),a=g(i),f=F(o,r),s={id:D++,comboStr:a,type:o,combo:i,handler:e,plugin:n,raw:l,options:f};$(a,o).push(s),p.set(s.id,{comboStr:a,type:o});const c=()=>m(s.id);return c.id=s.id,c.hotkey=l,c}function j(t,e){const{combo:n,type:r}=P(t),i=g(n),o=u.get(i);if(!o)return;const l=o.get(r);if(l?.length){if(e){const a=[];for(const f of l)f.handler===e?p.delete(f.id):a.push(f);a.length?o.set(r,a):o.delete(r)}else{for(const a of l)p.delete(a.id);o.delete(r)}o.size||u.delete(i)}}function N(t,e){if(d.has(t))throw new Error(`Plugin "${t}" already registered`);const n=[];for(const[r,i]of Object.entries(e)){const[o,l]=Array.isArray(i)?i:[i,{}],a=K(r,o,t,l);n.push(a.id)}return d.set(t,n),()=>M(t)}function M(t){const e=d.get(t);if(e){for(const n of e)m(n);d.delete(t)}}function W(){b=!0}function q(){b=!1}function H(t=!0){I=!!t}function R(t){h=t}function T(t){let e=null;for(const n of t)e?(n.options.priority>e.options.priority||n.options.priority===e.options.priority&&n.id>e.id)&&(e=n):e=n;return e}function S(t){return e=>{if(b)return;const n=I&&B(e),r=O(e),i=u.get(r)?.get(t);if(!i?.length)return;const o=[];for(const f of i){const s=f.options;n&&!s.allowIn?.(e)||t==="keydown"&&s.repeat===!1&&e.repeat||s.when&&s.when(e)===!1||o.push(f)}if(!o.length)return;const l=T(o),a=l.options;a.preventDefault&&e.preventDefault(),a.stopImmediatePropagation?e.stopImmediatePropagation():a.stopPropagation&&e.stopPropagation(),l.handler(e),a.once&&m(l.id)}}let y=null,w=null;function x({target:t=h,capture:e=!1}={}){if(!t)throw new Error("hotkeys.init() requires a target (e.g. window)");(y||w)&&A(),y=E(t,"keydown",S("keydown"),{capture:e}),w=E(t,"keyup",S("keyup"),{capture:e})}function A(){y?.(),w?.(),y=null,w=null,u.clear(),d.clear(),p.clear()}function X(t,{type:e}={}){const{combo:n,type:r}=P(t),i=g(n),o=e||r,l=u.get(i)?.get(o);if(!l?.length)return!1;const a={type:o,key:n.key,ctrlKey:!!n.ctrl,shiftKey:!!n.shift,altKey:!!n.alt,metaKey:!!n.meta,repeat:!1,preventDefault(){},stopPropagation(){},stopImmediatePropagation(){},target:null},f=T(l.filter(s=>s.options.when?s.options.when(a)!==!1:!0));return f?(f.handler(a),f.options.once&&m(f.id),!0):!1}h&&x();var G={init:x,destroy:A,setTarget:R,bind:K,unbind:j,registerPlugin:N,unregisterPlugin:M,pause:W,resume:q,ignoreInput:H,trigger:X};export{G as default};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hotkey-router",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A tiny, deterministic keyboard routing engine for modern web apps.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"author": "DR.WATT",
|
|
7
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"hotkeys",
|
|
10
|
+
"keyboard",
|
|
11
|
+
"shortcuts",
|
|
12
|
+
"router",
|
|
13
|
+
"javascript"
|
|
14
|
+
],
|
|
15
|
+
|
|
16
|
+
"main": "./dist/hotkey-router.cjs",
|
|
17
|
+
"module": "./dist/hotkey-router.js",
|
|
18
|
+
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"import": "./dist/hotkey-router.js",
|
|
22
|
+
"require": "./dist/hotkey-router.cjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "node build.js",
|
|
36
|
+
"prepublishOnly": "npm run build",
|
|
37
|
+
"test": "vitest run --environment jsdom",
|
|
38
|
+
"test:watch": "vitest --environment jsdom"
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"esbuild": "^0.21.0",
|
|
43
|
+
"jsdom": "^24.0.0",
|
|
44
|
+
"vitest": "^3.1.2"
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": ">=18"
|
|
49
|
+
}
|
|
50
|
+
}
|