routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
export type KeyCombination = Sequence | Alternate | Combo | Key;
|
||||
export interface Sequence {
|
||||
type: 'sequence';
|
||||
parts: (Alternate | Combo | Key)[];
|
||||
}
|
||||
export interface Alternate {
|
||||
type: 'alternate';
|
||||
parts: (Combo | Key)[];
|
||||
}
|
||||
export interface Combo {
|
||||
type: 'combo';
|
||||
parts: Key[];
|
||||
}
|
||||
export type Key = string;
|
||||
/**
|
||||
* Splits a single combination string into individual key parts.
|
||||
* Grammar:
|
||||
*
|
||||
* sequence = alternate *('-' alternate)
|
||||
* alternate = combo *('/' combo)
|
||||
* combo = key *(('+' | '_') key)
|
||||
* key = /./ *(/[^-/+_ ]/)
|
||||
*
|
||||
*/
|
||||
export declare function parseKeyCombination(input: string): KeyCombination;
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
// Utilities
|
||||
import { normalizeKey } from "./key-aliases.js";
|
||||
import { consoleWarn, includes } from "../../util/index.js"; // Types
|
||||
class ParseError extends Error {}
|
||||
|
||||
/**
|
||||
* Splits a single combination string into individual key parts.
|
||||
* Grammar:
|
||||
*
|
||||
* sequence = alternate *('-' alternate)
|
||||
* alternate = combo *('/' combo)
|
||||
* combo = key *(('+' | '_') key)
|
||||
* key = /./ *(/[^-/+_ ]/)
|
||||
*
|
||||
*/
|
||||
export function parseKeyCombination(input) {
|
||||
let pos = 0;
|
||||
try {
|
||||
const result = parseSequence();
|
||||
if (!atEnd()) {
|
||||
throw new ParseError(`Unexpected character '${peek()}' at position ${pos}`);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
if (err instanceof ParseError) {
|
||||
consoleWarn(`Invalid hotkey combination: ${err.message}\n ${input}\n ${' '.repeat(pos)}^`);
|
||||
return '';
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
function peek(ahead = 0) {
|
||||
return pos + ahead < input.length ? input[pos + ahead] : null;
|
||||
}
|
||||
function consume() {
|
||||
if (pos >= input.length) {
|
||||
throw new ParseError('Unexpected end of input');
|
||||
}
|
||||
return input[pos++];
|
||||
}
|
||||
function atEnd() {
|
||||
return pos >= input.length;
|
||||
}
|
||||
|
||||
// sequence = alternate *('-' alternate)
|
||||
function parseSequence() {
|
||||
const parts = [parseAlternate()];
|
||||
while (peek() === '-') {
|
||||
consume();
|
||||
parts.push(parseAlternate());
|
||||
}
|
||||
if (parts.length === 1) return parts[0];
|
||||
return {
|
||||
type: 'sequence',
|
||||
parts
|
||||
};
|
||||
}
|
||||
|
||||
// alternate = combo *('/' combo)
|
||||
function parseAlternate() {
|
||||
const parts = [parseCombo()];
|
||||
while (peek() === '/') {
|
||||
consume();
|
||||
parts.push(parseCombo());
|
||||
}
|
||||
if (parts.length === 1) return parts[0];
|
||||
return {
|
||||
type: 'alternate',
|
||||
parts
|
||||
};
|
||||
}
|
||||
|
||||
// combo = key *(('+' | '_') key)
|
||||
function parseCombo() {
|
||||
const keys = [parseKey()];
|
||||
while (includes(['+', '_'], peek())) {
|
||||
consume();
|
||||
keys.push(parseKey());
|
||||
}
|
||||
if (keys.length === 1) return keys[0];
|
||||
return {
|
||||
type: 'combo',
|
||||
parts: keys
|
||||
};
|
||||
}
|
||||
|
||||
// key = /./ *(/[^-/+_ ]/)
|
||||
function parseKey() {
|
||||
const ch = peek();
|
||||
if (ch == null) {
|
||||
throw new ParseError('Unexpected end of input');
|
||||
}
|
||||
const next = peek(1);
|
||||
if (isSep(ch) && next != null && !isSep(next)) {
|
||||
throw new ParseError(`Unexpected separator '${ch}' at position ${pos}`);
|
||||
}
|
||||
const first = consume();
|
||||
// separator keys are always a single character
|
||||
if (isSep(first)) return first;
|
||||
const chars = [first];
|
||||
while (!atEnd() && !isSep(peek()) && peek() !== ' ') {
|
||||
chars.push(consume());
|
||||
}
|
||||
return normalizeKey(chars.join(''));
|
||||
}
|
||||
}
|
||||
function isSep(char) {
|
||||
return includes(['-', '/', '+', '_'], char);
|
||||
}
|
||||
//# sourceMappingURL=hotkey-parsing.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+9
@@ -0,0 +1,9 @@
|
||||
import type { MaybeRef } from '../../util/index.js';
|
||||
interface HotkeyOptions {
|
||||
event?: MaybeRef<'keydown' | 'keyup'>;
|
||||
inputs?: MaybeRef<boolean>;
|
||||
preventDefault?: MaybeRef<boolean>;
|
||||
sequenceTimeout?: MaybeRef<number>;
|
||||
}
|
||||
export declare function useHotkey(keys: MaybeRef<string | undefined>, callback: (e: KeyboardEvent) => void, options?: HotkeyOptions): () => void;
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
// Composables
|
||||
import { parseKeyCombination } from "./hotkey-parsing.js"; // Utilities
|
||||
import { onScopeDispose, toValue, watch } from 'vue';
|
||||
import { IN_BROWSER } from "../../util/index.js"; // Types
|
||||
const MODIFIERS = ['ctrl', 'shift', 'alt', 'meta', 'cmd'];
|
||||
const modifiersSet = new Set(MODIFIERS);
|
||||
function isModifier(key) {
|
||||
return modifiersSet.has(key);
|
||||
}
|
||||
const emptyModifiers = Object.fromEntries(MODIFIERS.map(m => [m, false]));
|
||||
export function useHotkey(keys, callback, options = {}) {
|
||||
if (!IN_BROWSER) return function () {};
|
||||
const {
|
||||
event = 'keydown',
|
||||
inputs = false,
|
||||
preventDefault = true,
|
||||
sequenceTimeout = 1000
|
||||
} = options;
|
||||
const isMac = navigator?.userAgent?.includes('Macintosh') ?? false;
|
||||
let timeout = 0;
|
||||
let keyGroups;
|
||||
let isSequence = false;
|
||||
let groupIndex = 0;
|
||||
function isInputFocused() {
|
||||
if (toValue(inputs)) return false;
|
||||
const activeElement = document.activeElement;
|
||||
return activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.isContentEditable || activeElement.contentEditable === 'true');
|
||||
}
|
||||
function resetSequence() {
|
||||
groupIndex = 0;
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
function handler(e) {
|
||||
const group = keyGroups[groupIndex];
|
||||
if (!group || isInputFocused()) return;
|
||||
if (!matchesKeyGroup(e, group, isMac)) {
|
||||
if (isSequence) resetSequence();
|
||||
return;
|
||||
}
|
||||
if (toValue(preventDefault)) e.preventDefault();
|
||||
if (!isSequence) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
clearTimeout(timeout);
|
||||
groupIndex++;
|
||||
if (groupIndex === keyGroups.length) {
|
||||
callback(e);
|
||||
resetSequence();
|
||||
return;
|
||||
}
|
||||
timeout = window.setTimeout(resetSequence, toValue(sequenceTimeout));
|
||||
}
|
||||
function cleanup() {
|
||||
window.removeEventListener(toValue(event), handler);
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
watch(() => toValue(keys), newKeys => {
|
||||
cleanup();
|
||||
if (newKeys) {
|
||||
const parsed = parseKeyCombination(newKeys.toLowerCase());
|
||||
if (parsed) {
|
||||
const parts = typeof parsed !== 'string' && parsed.type === 'sequence' ? parsed.parts : [parsed];
|
||||
isSequence = parts.length > 1;
|
||||
keyGroups = parts;
|
||||
resetSequence();
|
||||
window.addEventListener(toValue(event), handler);
|
||||
}
|
||||
}
|
||||
}, {
|
||||
immediate: true
|
||||
});
|
||||
|
||||
// Watch for changes in the event type to re-register the listener
|
||||
watch(() => toValue(event), (newEvent, oldEvent) => {
|
||||
if (oldEvent && keyGroups && keyGroups.length > 0) {
|
||||
window.removeEventListener(oldEvent, handler);
|
||||
window.addEventListener(newEvent, handler);
|
||||
}
|
||||
});
|
||||
onScopeDispose(cleanup, true);
|
||||
return cleanup;
|
||||
}
|
||||
function matchesKeyGroup(e, group, isMac) {
|
||||
if (typeof group !== 'string' && group.type === 'alternate') {
|
||||
return group.parts.some(part => matchesKeyGroup(e, part, isMac));
|
||||
}
|
||||
const {
|
||||
modifiers,
|
||||
actualKey
|
||||
} = parseKeyGroup(group);
|
||||
const expectCtrl = modifiers.ctrl || !isMac && (modifiers.cmd || modifiers.meta);
|
||||
const expectMeta = isMac && (modifiers.cmd || modifiers.meta);
|
||||
return e.ctrlKey === expectCtrl && e.metaKey === expectMeta && e.shiftKey === modifiers.shift && e.altKey === modifiers.alt && e.key.toLowerCase() === actualKey?.toLowerCase();
|
||||
}
|
||||
function parseKeyGroup(group) {
|
||||
const parts = typeof group === 'string' ? [group] : group.parts;
|
||||
const modifiers = {
|
||||
...emptyModifiers
|
||||
};
|
||||
let actualKey;
|
||||
for (const part of parts) {
|
||||
if (isModifier(part)) {
|
||||
modifiers[part] = true;
|
||||
} else {
|
||||
// TODO: handle multiple keys
|
||||
actualKey = part;
|
||||
}
|
||||
}
|
||||
return {
|
||||
modifiers,
|
||||
actualKey
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=hotkey.js.map
|
||||
+1
File diff suppressed because one or more lines are too long
+1
@@ -0,0 +1 @@
|
||||
export { useHotkey } from './hotkey.js';
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export { useHotkey } from "./hotkey.js";
|
||||
//# sourceMappingURL=index.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","names":["useHotkey"],"sources":["../../../src/composables/hotkey/index.ts"],"sourcesContent":["export { useHotkey } from './hotkey'\n"],"mappings":"SAASA,SAAS","ignoreList":[]}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Centralized key alias mapping for consistent key normalization across the hotkey system.
|
||||
*
|
||||
* This maps various user-friendly aliases to canonical key names that match
|
||||
* KeyboardEvent.key values (in lowercase) where possible.
|
||||
*/
|
||||
export declare const keyAliasMap: Record<string, string>;
|
||||
/**
|
||||
* Normalizes a key string to its canonical form using the alias map.
|
||||
*
|
||||
* @param key - The key string to normalize
|
||||
* @returns The canonical key name in lowercase
|
||||
*/
|
||||
export declare function normalizeKey(key: string): string;
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Centralized key alias mapping for consistent key normalization across the hotkey system.
|
||||
*
|
||||
* This maps various user-friendly aliases to canonical key names that match
|
||||
* KeyboardEvent.key values (in lowercase) where possible.
|
||||
*/
|
||||
export const keyAliasMap = {
|
||||
// Modifier aliases (from vue-use, other libraries, and current implementation)
|
||||
control: 'ctrl',
|
||||
command: 'cmd',
|
||||
option: 'alt',
|
||||
// Arrow key aliases (common abbreviations)
|
||||
up: 'arrowup',
|
||||
down: 'arrowdown',
|
||||
left: 'arrowleft',
|
||||
right: 'arrowright',
|
||||
// Other common key aliases
|
||||
esc: 'escape',
|
||||
spacebar: ' ',
|
||||
space: ' ',
|
||||
return: 'enter',
|
||||
del: 'delete',
|
||||
// Symbol aliases (existing from hotkey-parsing.ts)
|
||||
plus: '+',
|
||||
slash: '/',
|
||||
underscore: '_',
|
||||
minus: '-',
|
||||
hyphen: '-'
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes a key string to its canonical form using the alias map.
|
||||
*
|
||||
* @param key - The key string to normalize
|
||||
* @returns The canonical key name in lowercase
|
||||
*/
|
||||
export function normalizeKey(key) {
|
||||
const lowerKey = key.toLowerCase();
|
||||
return keyAliasMap[lowerKey] || lowerKey;
|
||||
}
|
||||
//# sourceMappingURL=key-aliases.js.map
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"key-aliases.js","names":["keyAliasMap","control","command","option","up","down","left","right","esc","spacebar","space","return","del","plus","slash","underscore","minus","hyphen","normalizeKey","key","lowerKey","toLowerCase"],"sources":["../../../src/composables/hotkey/key-aliases.ts"],"sourcesContent":["/**\n * Centralized key alias mapping for consistent key normalization across the hotkey system.\n *\n * This maps various user-friendly aliases to canonical key names that match\n * KeyboardEvent.key values (in lowercase) where possible.\n */\nexport const keyAliasMap: Record<string, string> = {\n // Modifier aliases (from vue-use, other libraries, and current implementation)\n control: 'ctrl',\n command: 'cmd',\n option: 'alt',\n\n // Arrow key aliases (common abbreviations)\n up: 'arrowup',\n down: 'arrowdown',\n left: 'arrowleft',\n right: 'arrowright',\n\n // Other common key aliases\n esc: 'escape',\n spacebar: ' ',\n space: ' ',\n return: 'enter',\n del: 'delete',\n\n // Symbol aliases (existing from hotkey-parsing.ts)\n plus: '+',\n slash: '/',\n underscore: '_',\n minus: '-',\n hyphen: '-',\n}\n\n/**\n * Normalizes a key string to its canonical form using the alias map.\n *\n * @param key - The key string to normalize\n * @returns The canonical key name in lowercase\n */\nexport function normalizeKey (key: string): string {\n const lowerKey = key.toLowerCase()\n return keyAliasMap[lowerKey] || lowerKey\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMA,WAAmC,GAAG;EACjD;EACAC,OAAO,EAAE,MAAM;EACfC,OAAO,EAAE,KAAK;EACdC,MAAM,EAAE,KAAK;EAEb;EACAC,EAAE,EAAE,SAAS;EACbC,IAAI,EAAE,WAAW;EACjBC,IAAI,EAAE,WAAW;EACjBC,KAAK,EAAE,YAAY;EAEnB;EACAC,GAAG,EAAE,QAAQ;EACbC,QAAQ,EAAE,GAAG;EACbC,KAAK,EAAE,GAAG;EACVC,MAAM,EAAE,OAAO;EACfC,GAAG,EAAE,QAAQ;EAEb;EACAC,IAAI,EAAE,GAAG;EACTC,KAAK,EAAE,GAAG;EACVC,UAAU,EAAE,GAAG;EACfC,KAAK,EAAE,GAAG;EACVC,MAAM,EAAE;AACV,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,YAAYA,CAAEC,GAAW,EAAU;EACjD,MAAMC,QAAQ,GAAGD,GAAG,CAACE,WAAW,CAAC,CAAC;EAClC,OAAOrB,WAAW,CAACoB,QAAQ,CAAC,IAAIA,QAAQ;AAC1C","ignoreList":[]}
|
||||
Reference in New Issue
Block a user