mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2026-03-13 02:18:16 +01:00
shortcuts and config menu (#4530)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
191
frontend/src/utils/hotkeys.ts
Normal file
191
frontend/src/utils/hotkeys.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
import { KeyboardEvent as ReactKeyboardEvent } from 'react';
|
||||
|
||||
export interface HotkeyBinding {
|
||||
code: string;
|
||||
alt?: boolean;
|
||||
ctrl?: boolean;
|
||||
meta?: boolean;
|
||||
shift?: boolean;
|
||||
}
|
||||
|
||||
const MODIFIER_CODES = new Set([
|
||||
'ShiftLeft',
|
||||
'ShiftRight',
|
||||
'ControlLeft',
|
||||
'ControlRight',
|
||||
'AltLeft',
|
||||
'AltRight',
|
||||
'MetaLeft',
|
||||
'MetaRight',
|
||||
]);
|
||||
|
||||
const CODE_LABEL_MAP: Record<string, string> = {
|
||||
Minus: '-',
|
||||
Equal: '=',
|
||||
Backquote: '`',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
Backslash: '\\',
|
||||
IntlBackslash: '\\',
|
||||
Semicolon: ';',
|
||||
Quote: '\'',
|
||||
Comma: ',',
|
||||
Period: '.',
|
||||
Slash: '/',
|
||||
Space: 'Space',
|
||||
Tab: 'Tab',
|
||||
Escape: 'Esc',
|
||||
Enter: 'Enter',
|
||||
NumpadEnter: 'Num Enter',
|
||||
NumpadAdd: 'Num +',
|
||||
NumpadSubtract: 'Num -',
|
||||
NumpadMultiply: 'Num *',
|
||||
NumpadDivide: 'Num /',
|
||||
NumpadDecimal: 'Num .',
|
||||
NumpadComma: 'Num ,',
|
||||
NumpadEqual: 'Num =',
|
||||
};
|
||||
|
||||
export const isMacLike = (): boolean => {
|
||||
if (typeof navigator === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
const platform = navigator.platform?.toLowerCase() ?? '';
|
||||
const userAgent = navigator.userAgent?.toLowerCase() ?? '';
|
||||
return /mac|iphone|ipad|ipod/.test(platform) || /mac|iphone|ipad|ipod/.test(userAgent);
|
||||
};
|
||||
|
||||
export const isModifierCode = (code: string): boolean => MODIFIER_CODES.has(code);
|
||||
|
||||
const isFunctionKey = (code: string): boolean => /^F\d{1,2}$/.test(code);
|
||||
|
||||
export const bindingEquals = (a?: HotkeyBinding | null, b?: HotkeyBinding | null): boolean => {
|
||||
if (!a && !b) return true;
|
||||
if (!a || !b) return false;
|
||||
return (
|
||||
a.code === b.code &&
|
||||
Boolean(a.alt) === Boolean(b.alt) &&
|
||||
Boolean(a.ctrl) === Boolean(b.ctrl) &&
|
||||
Boolean(a.meta) === Boolean(b.meta) &&
|
||||
Boolean(a.shift) === Boolean(b.shift)
|
||||
);
|
||||
};
|
||||
|
||||
export const bindingMatchesEvent = (binding: HotkeyBinding, event: KeyboardEvent): boolean => {
|
||||
return (
|
||||
event.code === binding.code &&
|
||||
event.altKey === Boolean(binding.alt) &&
|
||||
event.ctrlKey === Boolean(binding.ctrl) &&
|
||||
event.metaKey === Boolean(binding.meta) &&
|
||||
event.shiftKey === Boolean(binding.shift)
|
||||
);
|
||||
};
|
||||
|
||||
export const eventToBinding = (event: KeyboardEvent | ReactKeyboardEvent): HotkeyBinding | null => {
|
||||
const code = event.code;
|
||||
if (!code || isModifierCode(code)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const binding: HotkeyBinding = {
|
||||
code,
|
||||
alt: event.altKey,
|
||||
ctrl: event.ctrlKey,
|
||||
meta: event.metaKey,
|
||||
shift: event.shiftKey,
|
||||
};
|
||||
|
||||
// Require at least one modifier to avoid clashing with text input
|
||||
if (!binding.alt && !binding.ctrl && !binding.meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return binding;
|
||||
};
|
||||
|
||||
const getKeyLabel = (code: string): string => {
|
||||
if (CODE_LABEL_MAP[code]) {
|
||||
return CODE_LABEL_MAP[code];
|
||||
}
|
||||
|
||||
if (code.startsWith('Key')) {
|
||||
return code.slice(3);
|
||||
}
|
||||
|
||||
if (code.startsWith('Digit')) {
|
||||
return code.slice(5);
|
||||
}
|
||||
|
||||
if (code.startsWith('Numpad')) {
|
||||
const remainder = code.slice(6);
|
||||
if (/^[0-9]$/.test(remainder)) {
|
||||
return `Num ${remainder}`;
|
||||
}
|
||||
return `Num ${remainder}`;
|
||||
}
|
||||
|
||||
// Match function keys (F1-F12)
|
||||
if (isFunctionKey(code)) {
|
||||
return code;
|
||||
}
|
||||
|
||||
switch (code) {
|
||||
case 'ArrowUp':
|
||||
return '↑';
|
||||
case 'ArrowDown':
|
||||
return '↓';
|
||||
case 'ArrowLeft':
|
||||
return '←';
|
||||
case 'ArrowRight':
|
||||
return '→';
|
||||
default:
|
||||
return code;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDisplayParts = (binding: HotkeyBinding | null | undefined, macLike: boolean): string[] => {
|
||||
if (!binding) return [];
|
||||
const parts: string[] = [];
|
||||
if (binding.meta) {
|
||||
parts.push(macLike ? '⌘' : 'Win');
|
||||
}
|
||||
if (binding.ctrl) {
|
||||
parts.push(macLike ? '⌃' : 'Ctrl');
|
||||
}
|
||||
if (binding.alt) {
|
||||
parts.push(macLike ? '⌥' : 'Alt');
|
||||
}
|
||||
if (binding.shift) {
|
||||
parts.push(macLike ? '⇧' : 'Shift');
|
||||
}
|
||||
parts.push(getKeyLabel(binding.code));
|
||||
return parts;
|
||||
};
|
||||
|
||||
export const serializeBindings = (bindings: Record<string, HotkeyBinding>): string => {
|
||||
return JSON.stringify(bindings);
|
||||
};
|
||||
|
||||
export const deserializeBindings = (value: string | null | undefined): Record<string, HotkeyBinding> => {
|
||||
if (!value) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(value) as Record<string, HotkeyBinding>;
|
||||
if (typeof parsed !== 'object' || parsed === null) {
|
||||
return {};
|
||||
}
|
||||
return parsed;
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse stored hotkey bindings', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
export const normalizeBinding = (binding: HotkeyBinding): HotkeyBinding => ({
|
||||
code: binding.code,
|
||||
alt: Boolean(binding.alt),
|
||||
ctrl: Boolean(binding.ctrl),
|
||||
meta: Boolean(binding.meta),
|
||||
shift: Boolean(binding.shift),
|
||||
});
|
||||
Reference in New Issue
Block a user