Files
NetMesh/application/state/pluginKeybindings.ts

137 lines
4.1 KiB
TypeScript
Raw Normal View History

export type PluginShortcutPlatform = 'mac' | 'windows' | 'linux';
const MODIFIER_ORDER = ['meta', 'ctrl', 'alt', 'shift'] as const;
const PRIMARY_MODIFIERS = new Set(['meta', 'ctrl']);
const NAMED_KEYS = new Map<string, string>([
[' ', 'space'],
['spacebar', 'space'],
['space', 'space'],
['arrowdown', 'arrowdown'],
['down', 'arrowdown'],
['arrowleft', 'arrowleft'],
['left', 'arrowleft'],
['arrowright', 'arrowright'],
['right', 'arrowright'],
['arrowup', 'arrowup'],
['up', 'arrowup'],
['backspace', 'backspace'],
['delete', 'delete'],
['del', 'delete'],
['end', 'end'],
['enter', 'enter'],
['return', 'enter'],
['escape', 'escape'],
['esc', 'escape'],
['home', 'home'],
['insert', 'insert'],
['minus', 'minus'],
['-', 'minus'],
['pagedown', 'pagedown'],
['pageup', 'pageup'],
['plus', 'plus'],
['+', 'plus'],
['tab', 'tab'],
]);
const PLUGIN_SHORTCUT_EDITABLE_SELECTOR = [
'input',
'textarea',
'select',
'[contenteditable]',
'[role="textbox"]',
'.monaco-editor',
'.monaco-diff-editor',
'.monaco-inputbox',
'.monaco-menu-container',
].join(', ');
function hasEditableShortcutAncestor(node: unknown): boolean {
const closest = (node as { closest?: (selector: string) => unknown } | null)?.closest;
return typeof closest === 'function'
&& Boolean(closest.call(node, PLUGIN_SHORTCUT_EDITABLE_SELECTOR));
}
export function isPluginShortcutEditableEvent(event: {
target: EventTarget | null;
composedPath?: () => EventTarget[];
}): boolean {
if (hasEditableShortcutAncestor(event.target)) return true;
return event.composedPath?.().some(hasEditableShortcutAncestor) ?? false;
}
function normalizeModifier(token: string, platform: PluginShortcutPlatform): string | null {
switch (token) {
case 'commandorcontrol':
case 'cmdorctrl':
case 'mod':
return platform === 'mac' ? 'meta' : 'ctrl';
case 'command':
case 'cmd':
case 'meta':
return 'meta';
case 'control':
case 'ctrl':
return 'ctrl';
case 'option':
case 'alt':
return 'alt';
case 'shift':
return 'shift';
default:
return null;
}
}
function normalizeKey(token: string): string | null {
const lower = token.toLowerCase();
if (/^[a-z0-9]$/u.test(lower) || /^f(?:[1-9]|1[0-9]|2[0-4])$/u.test(lower)) return lower;
return NAMED_KEYS.get(lower) ?? null;
}
export function resolvePluginShortcutPlatform(platform: string): PluginShortcutPlatform {
if (/Mac|iPhone|iPad/u.test(platform)) return 'mac';
if (/Win/u.test(platform)) return 'windows';
return 'linux';
}
export function normalizePluginShortcut(
shortcut: string,
platform: PluginShortcutPlatform,
): string | null {
if (typeof shortcut !== 'string' || shortcut.length < 1 || shortcut.length > 128) return null;
const tokens = shortcut.split('+').map((token) => token.trim());
if (tokens.some((token) => token.length === 0) || tokens.length > 5) return null;
const modifiers = new Set<string>();
let key: string | null = null;
for (const token of tokens) {
const modifier = normalizeModifier(token.toLowerCase(), platform);
if (modifier) {
if (key || modifiers.has(modifier)
|| (PRIMARY_MODIFIERS.has(modifier)
&& [...modifiers].some((item) => PRIMARY_MODIFIERS.has(item)))) return null;
modifiers.add(modifier);
continue;
}
if (key) return null;
key = normalizeKey(token);
if (!key) return null;
}
if (!key) return null;
return [...MODIFIER_ORDER.filter((modifier) => modifiers.has(modifier)), key].join('+');
}
export function normalizePluginKeyboardEvent(event: Pick<KeyboardEvent,
'key' | 'metaKey' | 'ctrlKey' | 'altKey' | 'shiftKey'> & { code?: string }): string | null {
const codeKey = event.code?.match(/^Key([A-Z])$/u)?.[1]?.toLowerCase()
?? event.code?.match(/^Digit([0-9])$/u)?.[1]
?? null;
const key = normalizeKey(event.key) ?? codeKey;
if (!key) return null;
return [
event.metaKey ? 'meta' : '',
event.ctrlKey ? 'ctrl' : '',
event.altKey ? 'alt' : '',
event.shiftKey ? 'shift' : '',
key,
].filter(Boolean).join('+');
}