Major overhaul to the theme and now functioning.
This commit is contained in:
Generated
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
// Types
|
||||
export declare const MODIFIERS: string[];
|
||||
/**
|
||||
* Splits a single combination string into individual key parts.
|
||||
*
|
||||
* A combination is a set of keys that must be pressed simultaneously.
|
||||
* e.g. `ctrl+k`, `shift--`
|
||||
*/
|
||||
export declare function splitKeyCombination(combination: string, isInternal?: boolean): {
|
||||
keys: string[];
|
||||
separators: string[];
|
||||
};
|
||||
/**
|
||||
* Splits a hotkey string into its constituent combination groups.
|
||||
*
|
||||
* A sequence is a series of combinations that must be pressed in order.
|
||||
* e.g. `a-b`, `ctrl+k-p`
|
||||
*/
|
||||
export declare function splitKeySequence(str: string): string[];
|
||||
Generated
Vendored
+164
@@ -0,0 +1,164 @@
|
||||
// Utilities
|
||||
import { normalizeKey } from "./key-aliases.js";
|
||||
import { consoleWarn } from "../../util/console.js"; // Types
|
||||
export const MODIFIERS = ['ctrl', 'shift', 'alt', 'meta', 'cmd'];
|
||||
|
||||
/**
|
||||
* Splits a single combination string into individual key parts.
|
||||
*
|
||||
* A combination is a set of keys that must be pressed simultaneously.
|
||||
* e.g. `ctrl+k`, `shift--`
|
||||
*/
|
||||
export function splitKeyCombination(combination) {
|
||||
let isInternal = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
|
||||
const emptyResult = {
|
||||
keys: [],
|
||||
separators: []
|
||||
};
|
||||
if (!combination) {
|
||||
if (!isInternal) consoleWarn('Invalid hotkey combination: empty string provided');
|
||||
return emptyResult;
|
||||
}
|
||||
|
||||
// --- VALIDATION ---
|
||||
|
||||
const hasInvalidLeadingSeparator = combination.length > 1 &&
|
||||
// Starts with a single separator followed by a non-separator character (e.g. '+a', '_a')
|
||||
['+', '/', '_'].some(v => combination.startsWith(v)) && !['++', '//', '__'].some(v => combination.startsWith(v));
|
||||
const hasInvalidStructure = hasInvalidLeadingSeparator ||
|
||||
// Disallow literal + or _ keys (they require shift)
|
||||
combination.includes('++') || combination.includes('__') || combination === '+' || combination === '_' ||
|
||||
// Ends with a separator that is not part of a doubled literal
|
||||
combination.length > 1 && (combination.endsWith('+') || combination.endsWith('_')) && combination.at(-2) !== combination.at(-1) ||
|
||||
// Stand-alone doubled separators (dangling)
|
||||
combination === '++' || combination === '--' || combination === '__';
|
||||
if (hasInvalidStructure) {
|
||||
if (!isInternal) consoleWarn(`Invalid hotkey combination: "${combination}" has invalid structure`);
|
||||
return emptyResult;
|
||||
}
|
||||
const keys = [];
|
||||
const separators = [];
|
||||
let buffer = '';
|
||||
const flushBuffer = separator => {
|
||||
if (buffer) {
|
||||
separator && separators.push(separator);
|
||||
keys.push(normalizeKey(buffer));
|
||||
buffer = '';
|
||||
}
|
||||
};
|
||||
for (let i = 0; i < combination.length; i++) {
|
||||
const char = combination[i];
|
||||
const nextChar = combination[i + 1];
|
||||
if (['+', '/', '_', '-'].includes(char)) {
|
||||
if (char === nextChar) {
|
||||
flushBuffer(char);
|
||||
keys.push(char);
|
||||
i++;
|
||||
} else if (['+', '/', '_'].includes(char)) {
|
||||
flushBuffer(char);
|
||||
} else {
|
||||
buffer += char;
|
||||
}
|
||||
} else {
|
||||
buffer += char;
|
||||
}
|
||||
}
|
||||
flushBuffer();
|
||||
|
||||
// Within a combination, `-` is only valid as a literal key (e.g., `ctrl+-`).
|
||||
// `-` cannot be part of a longer key name within a combination.
|
||||
const hasInvalidMinus = keys.some(key => key.length > 1 && key.includes('-') && key !== '--');
|
||||
if (hasInvalidMinus) {
|
||||
if (!isInternal) consoleWarn(`Invalid hotkey combination: "${combination}" has invalid structure`);
|
||||
return emptyResult;
|
||||
}
|
||||
if (keys.length === 0 && combination) {
|
||||
return {
|
||||
keys: [normalizeKey(combination)],
|
||||
separators
|
||||
};
|
||||
}
|
||||
return {
|
||||
keys,
|
||||
separators
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a hotkey string into its constituent combination groups.
|
||||
*
|
||||
* A sequence is a series of combinations that must be pressed in order.
|
||||
* e.g. `a-b`, `ctrl+k-p`
|
||||
*/
|
||||
export function splitKeySequence(str) {
|
||||
if (!str) {
|
||||
consoleWarn('Invalid hotkey sequence: empty string provided');
|
||||
return [];
|
||||
}
|
||||
|
||||
// A sequence is invalid if it starts or ends with a separator,
|
||||
// unless it is part of a combination (e.g., `shift+-`).
|
||||
const hasInvalidStart = str.startsWith('-') && !['---', '--+'].includes(str);
|
||||
const hasInvalidEnd = str.endsWith('-') && !str.endsWith('+-') && !str.endsWith('_-') && str !== '-' && str !== '---';
|
||||
if (hasInvalidStart || hasInvalidEnd) {
|
||||
consoleWarn(`Invalid hotkey sequence: "${str}" contains invalid combinations`);
|
||||
return [];
|
||||
}
|
||||
const result = [];
|
||||
let buffer = '';
|
||||
let i = 0;
|
||||
while (i < str.length) {
|
||||
const char = str[i];
|
||||
if (char === '-') {
|
||||
// Determine if this hyphen is part of the current combination
|
||||
const prevChar = str[i - 1];
|
||||
const prevPrevChar = i > 1 ? str[i - 2] : undefined;
|
||||
const precededBySeparator = ['+', '_'].includes(prevChar) && !['+', '/'].includes(prevPrevChar ?? '');
|
||||
if (precededBySeparator) {
|
||||
// Treat as part of the combination (e.g., 'ctrl+-')
|
||||
buffer += char;
|
||||
i++;
|
||||
} else {
|
||||
// Treat as sequence separator
|
||||
if (buffer) {
|
||||
result.push(buffer);
|
||||
buffer = '';
|
||||
} else {
|
||||
// Empty buffer means we have a literal '-' key
|
||||
result.push('-');
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
buffer += char;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
// Add final buffer if it exists
|
||||
if (buffer) {
|
||||
result.push(buffer);
|
||||
}
|
||||
|
||||
// Collapse runs of '-' so that every second '-' is removed
|
||||
const collapsed = [];
|
||||
let minusCount = 0;
|
||||
for (const part of result) {
|
||||
if (part === '-') {
|
||||
if (minusCount % 2 === 0) collapsed.push('-');
|
||||
minusCount++;
|
||||
} else {
|
||||
minusCount = 0;
|
||||
collapsed.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that each part of the sequence is a valid combination
|
||||
const areAllValid = collapsed.every(s => splitKeyCombination(s, true).keys.length > 0);
|
||||
if (!areAllValid) {
|
||||
consoleWarn(`Invalid hotkey sequence: "${str}" contains invalid combinations`);
|
||||
return [];
|
||||
}
|
||||
return collapsed;
|
||||
}
|
||||
//# sourceMappingURL=hotkey-parsing.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
// Types
|
||||
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;
|
||||
|
||||
Generated
Vendored
+114
@@ -0,0 +1,114 @@
|
||||
// Composables
|
||||
import { splitKeyCombination, splitKeySequence } from "./hotkey-parsing.js"; // Utilities
|
||||
import { onScopeDispose, toValue, watch } from 'vue';
|
||||
import { IN_BROWSER } from "../../util/index.js"; // Types
|
||||
export function useHotkey(keys, callback) {
|
||||
let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
||||
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 groups = splitKeySequence(newKeys.toLowerCase());
|
||||
isSequence = groups.length > 1;
|
||||
keyGroups = groups;
|
||||
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) {
|
||||
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 MODIFIERS = ['ctrl', 'shift', 'alt', 'meta', 'cmd'];
|
||||
|
||||
// Use the shared combination splitting logic
|
||||
const {
|
||||
keys: parts
|
||||
} = splitKeyCombination(group.toLowerCase());
|
||||
|
||||
// If the combination is invalid, return empty result
|
||||
if (parts.length === 0) {
|
||||
return {
|
||||
modifiers: Object.fromEntries(MODIFIERS.map(m => [m, false])),
|
||||
actualKey: undefined
|
||||
};
|
||||
}
|
||||
const modifiers = Object.fromEntries(MODIFIERS.map(m => [m, false]));
|
||||
let actualKey;
|
||||
for (const part of parts) {
|
||||
if (MODIFIERS.includes(part)) {
|
||||
modifiers[part] = true;
|
||||
} else {
|
||||
actualKey = part;
|
||||
}
|
||||
}
|
||||
return {
|
||||
modifiers,
|
||||
actualKey
|
||||
};
|
||||
}
|
||||
//# sourceMappingURL=hotkey.js.map
|
||||
Generated
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export { useHotkey } from './hotkey.js';
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export { useHotkey } from "./hotkey.js";
|
||||
//# sourceMappingURL=index.js.map
|
||||
Generated
Vendored
+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":[]}
|
||||
Generated
Vendored
+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;
|
||||
Generated
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 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)
|
||||
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
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"key-aliases.js","names":["keyAliasMap","control","command","option","up","down","left","right","esc","spacebar","space","return","del","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 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,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,OAAOlB,WAAW,CAACiB,QAAQ,CAAC,IAAIA,QAAQ;AAC1C","ignoreList":[]}
|
||||
Reference in New Issue
Block a user