Files
rfcp/frontend/src/hooks/useKeyboardShortcuts.ts
2026-02-06 22:17:24 +02:00

148 lines
4.9 KiB
TypeScript

import { useEffect } from 'react';
import { useSitesStore } from '@/store/sites.ts';
import { useCoverageStore } from '@/store/coverage.ts';
import { useSettingsStore } from '@/store/settings.ts';
import { useToolStore } from '@/store/tools.ts';
import { useToastStore } from '@/components/ui/Toast.tsx';
interface ShortcutHandlers {
onCalculate: () => void;
onCloseForm: () => void;
onShowShortcuts?: () => void;
onDeleteRequest?: (siteId: string, siteName: string) => void;
onUndo?: () => void;
onRedo?: () => void;
}
function isInputActive(): boolean {
const el = document.activeElement;
if (!el) return false;
const tag = el.tagName;
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
}
export function useKeyboardShortcuts({
onCalculate,
onCloseForm,
onShowShortcuts,
onDeleteRequest,
onUndo,
onRedo,
}: ShortcutHandlers) {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
const isMac = navigator.platform.toLowerCase().includes('mac');
const modKey = isMac ? e.metaKey : e.ctrlKey;
// === Modifier shortcuts (work even in inputs) ===
if (modKey && e.key === 'Enter') {
e.preventDefault();
onCalculate();
return;
}
// Undo/Redo: use e.code (layout-independent) as primary, e.key as fallback.
// e.key can be uppercase 'Z' with Shift, or a control char on some layouts.
// e.code is always 'KeyZ' / 'KeyY' regardless of modifier or layout.
const isZ = e.code === 'KeyZ' || e.key.toLowerCase() === 'z';
const isY = e.code === 'KeyY' || e.key.toLowerCase() === 'y';
// Undo: Ctrl+Z (or Cmd+Z on Mac)
if (modKey && isZ && !e.shiftKey) {
e.preventDefault();
onUndo?.();
return;
}
// Redo: Ctrl+Shift+Z or Ctrl+Y (Cmd+Shift+Z or Cmd+Y on Mac)
if (modKey && ((isZ && e.shiftKey) || isY)) {
e.preventDefault();
onRedo?.();
return;
}
// Escape always works
if (e.key === 'Escape') {
useSitesStore.getState().selectSite(null);
useToolStore.getState().clearTool();
onCloseForm();
return;
}
// === Skip remaining shortcuts if typing in an input ===
if (isInputActive()) return;
// Shift combos (no browser conflicts)
if (e.shiftKey && !modKey && !e.altKey) {
switch (e.key.toUpperCase()) {
case 'S': // Shift+S: New site (place mode)
e.preventDefault();
useToolStore.getState().setActiveTool('site-placement');
useToastStore.getState().addToast('Click on map to place new site', 'info');
return;
case 'C': // Shift+C: Clear coverage
e.preventDefault();
useCoverageStore.getState().clearCoverage();
useToastStore.getState().addToast('Coverage cleared', 'info');
return;
}
}
// Single letter shortcuts (no modifiers)
if (!modKey && !e.altKey && !e.shiftKey) {
switch (e.key.toLowerCase()) {
case 'h': // H: Toggle heatmap
useCoverageStore.getState().toggleHeatmap();
return;
case 'g': // G: Toggle grid
useSettingsStore.getState().setShowGrid(
!useSettingsStore.getState().showGrid
);
return;
case 't': // T: Toggle terrain
useSettingsStore.getState().setShowElevationOverlay(
!useSettingsStore.getState().showElevationOverlay
);
return;
case 'r': // R: Toggle ruler / measurement
useSettingsStore.getState().setMeasurementMode(
!useSettingsStore.getState().measurementMode
);
return;
case 'f': // F: Fit to coverage — dispatch custom event for Map.tsx to handle
window.dispatchEvent(new CustomEvent('rfcp:fit-bounds'));
return;
case 'delete': // Delete key: show confirmation dialog
case 'backspace':
{
const selectedId = useSitesStore.getState().selectedSiteId;
if (selectedId && onDeleteRequest) {
const site = useSitesStore.getState().sites.find(s => s.id === selectedId);
if (site) {
e.preventDefault();
onDeleteRequest(site.id, site.name);
}
}
}
return;
}
// ? key for help (not shift on some layouts)
if (e.key === '?') {
onShowShortcuts?.();
return;
}
}
// ? with shift (most layouts: shift+/)
if (e.shiftKey && !modKey && !e.altKey && e.key === '?') {
onShowShortcuts?.();
return;
}
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onCalculate, onCloseForm, onShowShortcuts, onDeleteRequest, onUndo, onRedo]);
}