Menu
Public repository / fee5b6c2cde89fa6bf8dc51068af7336b56a88b8
AkurAI-Framework / site/frontend/hotkeys.js
Latest change 5097caa5d055c8539c451aeda1036baafb1d495f - OLI-245: hotkeys.js helper + SPA router/IdP return_to docs by Ólafur Búi Ólafsson
// hotkeys.js — keyboard-shortcut and focus-scope helper for AkurAI/Framework.
//
// Native ES module, no dependencies. Pure `resolveHotkey` maps a keydown to an
// action name; `createHotkeys` binds one listener and routes actions.
//
// Keymap groups:
// global: { key: action } always active
// selection: { key: action } active only when ctx.hasSelection
// <other>: { scope: "css", keys: {...} } active only when the event target
// is inside an element matching `scope`
// Plain-modifier chords (Ctrl/Meta/Alt) are never handled. Typing targets
// (input/textarea/select/contenteditable) are ignored; Escape there yields
// "closeTyping" so callers can blur. When disabled, only Escape still resolves.
export function isTypingTarget(t) {
return !!t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.tagName === "SELECT" || t.isContentEditable === true);
}
function lookup(map, key) {
return map && Object.prototype.hasOwnProperty.call(map, key) ? map[key] : null;
}
export function resolveHotkey(e, keymap, { enabled = true, hasSelection = false } = {}) {
if (e.metaKey || e.ctrlKey || e.altKey) return null;
if (!enabled && e.key !== "Escape") return null;
if (isTypingTarget(e.target)) return e.key === "Escape" ? "closeTyping" : null;
for (const [name, group] of Object.entries(keymap)) {
let keys = group;
if (group.keys) {
if (!e.target?.closest?.(group.scope)) continue;
keys = group.keys;
} else if (name === "selection" && !hasSelection) continue;
const action = lookup(keys, e.key);
if (action) return action;
}
return null;
}
// Binds one keydown listener; `enabled` is toggleable via setEnabled.
export function createHotkeys({ window: win, keymap, onAction, hasSelection = () => false, preventDefault = [], enabled = true, persist }) {
let on = enabled;
const prevent = new Set(preventDefault);
const onKey = (e) => {
const action = resolveHotkey(e, keymap, { enabled: on, hasSelection: hasSelection() });
if (!action) return;
if (prevent.has(action)) e.preventDefault();
onAction(action, e);
};
win.addEventListener("keydown", onKey);
return {
enabled: () => on,
setEnabled(value) {
on = !!value;
if (persist) persist(on);
},
destroy: () => win.removeEventListener("keydown", onKey),
};
}